Import giada_0.21.0.orig.tar.gz
authorDennis Braun <d_braun@kabelmail.de>
Sun, 15 May 2022 16:50:55 +0000 (18:50 +0200)
committerDennis Braun <d_braun@kabelmail.de>
Sun, 15 May 2022 16:50:55 +0000 (18:50 +0200)
[dgit import orig giada_0.21.0.orig.tar.gz]

381 files changed:
.clang-format [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
README.md [new file with mode: 0644]
extras/com.giadamusic.Giada.desktop [new file with mode: 0644]
extras/com.giadamusic.Giada.metainfo.xml [new file with mode: 0644]
extras/giada-logo.png [new file with mode: 0644]
extras/giada-logo.svg [new file with mode: 0644]
extras/giada-logotype.png [new file with mode: 0644]
extras/giada.icns [new file with mode: 0644]
scripts/create_source_tarball.sh [new file with mode: 0755]
src/core/actions/action.h [new file with mode: 0644]
src/core/actions/actionRecorder.cpp [new file with mode: 0644]
src/core/actions/actionRecorder.h [new file with mode: 0644]
src/core/actions/actions.cpp [new file with mode: 0644]
src/core/actions/actions.h [new file with mode: 0644]
src/core/channels/audioReceiver.cpp [new file with mode: 0644]
src/core/channels/audioReceiver.h [new file with mode: 0644]
src/core/channels/channel.cpp [new file with mode: 0644]
src/core/channels/channel.h [new file with mode: 0644]
src/core/channels/channelManager.cpp [new file with mode: 0644]
src/core/channels/channelManager.h [new file with mode: 0644]
src/core/channels/midiActionRecorder.cpp [new file with mode: 0644]
src/core/channels/midiActionRecorder.h [new file with mode: 0644]
src/core/channels/midiController.cpp [new file with mode: 0644]
src/core/channels/midiController.h [new file with mode: 0644]
src/core/channels/midiLearner.cpp [new file with mode: 0644]
src/core/channels/midiLearner.h [new file with mode: 0644]
src/core/channels/midiLighter.cpp [new file with mode: 0644]
src/core/channels/midiLighter.h [new file with mode: 0644]
src/core/channels/midiReceiver.cpp [new file with mode: 0644]
src/core/channels/midiReceiver.h [new file with mode: 0644]
src/core/channels/midiSender.cpp [new file with mode: 0644]
src/core/channels/midiSender.h [new file with mode: 0644]
src/core/channels/sampleActionRecorder.cpp [new file with mode: 0644]
src/core/channels/sampleActionRecorder.h [new file with mode: 0644]
src/core/channels/sampleAdvancer.cpp [new file with mode: 0644]
src/core/channels/sampleAdvancer.h [new file with mode: 0644]
src/core/channels/samplePlayer.cpp [new file with mode: 0644]
src/core/channels/samplePlayer.h [new file with mode: 0644]
src/core/channels/sampleReactor.cpp [new file with mode: 0644]
src/core/channels/sampleReactor.h [new file with mode: 0644]
src/core/channels/waveReader.cpp [new file with mode: 0644]
src/core/channels/waveReader.h [new file with mode: 0644]
src/core/conf.cpp [new file with mode: 0644]
src/core/conf.h [new file with mode: 0644]
src/core/const.h [new file with mode: 0644]
src/core/engine.cpp [new file with mode: 0644]
src/core/engine.h [new file with mode: 0644]
src/core/eventDispatcher.cpp [new file with mode: 0644]
src/core/eventDispatcher.h [new file with mode: 0644]
src/core/graphics.cpp [new file with mode: 0644]
src/core/graphics.h [new file with mode: 0644]
src/core/idManager.cpp [new file with mode: 0644]
src/core/idManager.h [new file with mode: 0644]
src/core/init.cpp [new file with mode: 0644]
src/core/init.h [new file with mode: 0644]
src/core/jackTransport.cpp [new file with mode: 0644]
src/core/jackTransport.h [new file with mode: 0644]
src/core/kernelAudio.cpp [new file with mode: 0644]
src/core/kernelAudio.h [new file with mode: 0644]
src/core/kernelMidi.cpp [new file with mode: 0644]
src/core/kernelMidi.h [new file with mode: 0644]
src/core/metronome.cpp [new file with mode: 0644]
src/core/metronome.h [new file with mode: 0644]
src/core/midiDispatcher.cpp [new file with mode: 0644]
src/core/midiDispatcher.h [new file with mode: 0644]
src/core/midiEvent.cpp [new file with mode: 0644]
src/core/midiEvent.h [new file with mode: 0644]
src/core/midiLearnParam.cpp [new file with mode: 0644]
src/core/midiLearnParam.h [new file with mode: 0644]
src/core/midiMapper.cpp [new file with mode: 0644]
src/core/midiMapper.h [new file with mode: 0644]
src/core/mixer.cpp [new file with mode: 0644]
src/core/mixer.h [new file with mode: 0644]
src/core/mixerHandler.cpp [new file with mode: 0644]
src/core/mixerHandler.h [new file with mode: 0644]
src/core/model/mixer.cpp [new file with mode: 0644]
src/core/model/mixer.h [new file with mode: 0644]
src/core/model/model.cpp [new file with mode: 0644]
src/core/model/model.h [new file with mode: 0644]
src/core/model/recorder.cpp [new file with mode: 0644]
src/core/model/recorder.h [new file with mode: 0644]
src/core/model/sequencer.cpp [new file with mode: 0644]
src/core/model/sequencer.h [new file with mode: 0644]
src/core/model/storage.cpp [new file with mode: 0644]
src/core/model/storage.h [new file with mode: 0644]
src/core/patch.cpp [new file with mode: 0644]
src/core/patch.h [new file with mode: 0644]
src/core/plugins/plugin.cpp [new file with mode: 0644]
src/core/plugins/plugin.h [new file with mode: 0644]
src/core/plugins/pluginHost.cpp [new file with mode: 0644]
src/core/plugins/pluginHost.h [new file with mode: 0644]
src/core/plugins/pluginManager.cpp [new file with mode: 0644]
src/core/plugins/pluginManager.h [new file with mode: 0644]
src/core/plugins/pluginState.cpp [new file with mode: 0644]
src/core/plugins/pluginState.h [new file with mode: 0644]
src/core/quantizer.cpp [new file with mode: 0644]
src/core/quantizer.h [new file with mode: 0644]
src/core/queue.h [new file with mode: 0644]
src/core/range.h [new file with mode: 0644]
src/core/recorder.cpp [new file with mode: 0644]
src/core/recorder.h [new file with mode: 0644]
src/core/resampler.cpp [new file with mode: 0644]
src/core/resampler.h [new file with mode: 0644]
src/core/ringBuffer.h [new file with mode: 0644]
src/core/sequencer.cpp [new file with mode: 0644]
src/core/sequencer.h [new file with mode: 0644]
src/core/synchronizer.cpp [new file with mode: 0644]
src/core/synchronizer.h [new file with mode: 0644]
src/core/types.h [new file with mode: 0644]
src/core/wave.cpp [new file with mode: 0644]
src/core/wave.h [new file with mode: 0644]
src/core/waveFx.cpp [new file with mode: 0644]
src/core/waveFx.h [new file with mode: 0644]
src/core/waveManager.cpp [new file with mode: 0644]
src/core/waveManager.h [new file with mode: 0644]
src/core/weakAtomic.h [new file with mode: 0644]
src/core/worker.cpp [new file with mode: 0644]
src/core/worker.h [new file with mode: 0644]
src/deps/juce-config.h [new file with mode: 0644]
src/ext/giada.ico [new file with mode: 0644]
src/ext/resource.h [new file with mode: 0644]
src/ext/resource.rc [new file with mode: 0644]
src/glue/actionEditor.cpp [new file with mode: 0644]
src/glue/actionEditor.h [new file with mode: 0644]
src/glue/channel.cpp [new file with mode: 0644]
src/glue/channel.h [new file with mode: 0644]
src/glue/config.cpp [new file with mode: 0644]
src/glue/config.h [new file with mode: 0644]
src/glue/events.cpp [new file with mode: 0644]
src/glue/events.h [new file with mode: 0644]
src/glue/io.cpp [new file with mode: 0644]
src/glue/io.h [new file with mode: 0644]
src/glue/layout.cpp [new file with mode: 0644]
src/glue/layout.h [new file with mode: 0644]
src/glue/main.cpp [new file with mode: 0644]
src/glue/main.h [new file with mode: 0644]
src/glue/plugin.cpp [new file with mode: 0644]
src/glue/plugin.h [new file with mode: 0644]
src/glue/recorder.cpp [new file with mode: 0644]
src/glue/recorder.h [new file with mode: 0644]
src/glue/sampleEditor.cpp [new file with mode: 0644]
src/glue/sampleEditor.h [new file with mode: 0644]
src/glue/storage.cpp [new file with mode: 0644]
src/glue/storage.h [new file with mode: 0644]
src/gui/dialogs/about.cpp [new file with mode: 0644]
src/gui/dialogs/about.h [new file with mode: 0644]
src/gui/dialogs/actionEditor/baseActionEditor.cpp [new file with mode: 0644]
src/gui/dialogs/actionEditor/baseActionEditor.h [new file with mode: 0644]
src/gui/dialogs/actionEditor/midiActionEditor.cpp [new file with mode: 0644]
src/gui/dialogs/actionEditor/midiActionEditor.h [new file with mode: 0644]
src/gui/dialogs/actionEditor/sampleActionEditor.cpp [new file with mode: 0644]
src/gui/dialogs/actionEditor/sampleActionEditor.h [new file with mode: 0644]
src/gui/dialogs/beatsInput.cpp [new file with mode: 0644]
src/gui/dialogs/beatsInput.h [new file with mode: 0644]
src/gui/dialogs/bpmInput.cpp [new file with mode: 0644]
src/gui/dialogs/bpmInput.h [new file with mode: 0644]
src/gui/dialogs/browser/browserBase.cpp [new file with mode: 0644]
src/gui/dialogs/browser/browserBase.h [new file with mode: 0644]
src/gui/dialogs/browser/browserDir.cpp [new file with mode: 0644]
src/gui/dialogs/browser/browserDir.h [new file with mode: 0644]
src/gui/dialogs/browser/browserLoad.cpp [new file with mode: 0644]
src/gui/dialogs/browser/browserLoad.h [new file with mode: 0644]
src/gui/dialogs/browser/browserSave.cpp [new file with mode: 0644]
src/gui/dialogs/browser/browserSave.h [new file with mode: 0644]
src/gui/dialogs/channelNameInput.cpp [new file with mode: 0644]
src/gui/dialogs/channelNameInput.h [new file with mode: 0644]
src/gui/dialogs/config.cpp [new file with mode: 0644]
src/gui/dialogs/config.h [new file with mode: 0644]
src/gui/dialogs/keyGrabber.cpp [new file with mode: 0644]
src/gui/dialogs/keyGrabber.h [new file with mode: 0644]
src/gui/dialogs/mainWindow.cpp [new file with mode: 0644]
src/gui/dialogs/mainWindow.h [new file with mode: 0644]
src/gui/dialogs/midiIO/midiInputBase.cpp [new file with mode: 0644]
src/gui/dialogs/midiIO/midiInputBase.h [new file with mode: 0644]
src/gui/dialogs/midiIO/midiInputChannel.cpp [new file with mode: 0644]
src/gui/dialogs/midiIO/midiInputChannel.h [new file with mode: 0644]
src/gui/dialogs/midiIO/midiInputMaster.cpp [new file with mode: 0644]
src/gui/dialogs/midiIO/midiInputMaster.h [new file with mode: 0644]
src/gui/dialogs/midiIO/midiOutputBase.cpp [new file with mode: 0644]
src/gui/dialogs/midiIO/midiOutputBase.h [new file with mode: 0644]
src/gui/dialogs/midiIO/midiOutputMidiCh.cpp [new file with mode: 0644]
src/gui/dialogs/midiIO/midiOutputMidiCh.h [new file with mode: 0644]
src/gui/dialogs/midiIO/midiOutputSampleCh.cpp [new file with mode: 0644]
src/gui/dialogs/midiIO/midiOutputSampleCh.h [new file with mode: 0644]
src/gui/dialogs/missingAssets.cpp [new file with mode: 0644]
src/gui/dialogs/missingAssets.h [new file with mode: 0644]
src/gui/dialogs/pluginChooser.cpp [new file with mode: 0644]
src/gui/dialogs/pluginChooser.h [new file with mode: 0644]
src/gui/dialogs/pluginList.cpp [new file with mode: 0644]
src/gui/dialogs/pluginList.h [new file with mode: 0644]
src/gui/dialogs/pluginWindow.cpp [new file with mode: 0644]
src/gui/dialogs/pluginWindow.h [new file with mode: 0644]
src/gui/dialogs/pluginWindowGUI.cpp [new file with mode: 0644]
src/gui/dialogs/pluginWindowGUI.h [new file with mode: 0644]
src/gui/dialogs/progress.cpp [new file with mode: 0644]
src/gui/dialogs/progress.h [new file with mode: 0644]
src/gui/dialogs/sampleEditor.cpp [new file with mode: 0644]
src/gui/dialogs/sampleEditor.h [new file with mode: 0644]
src/gui/dialogs/warnings.cpp [new file with mode: 0644]
src/gui/dialogs/warnings.h [new file with mode: 0644]
src/gui/dialogs/window.cpp [new file with mode: 0644]
src/gui/dialogs/window.h [new file with mode: 0644]
src/gui/dispatcher.cpp [new file with mode: 0644]
src/gui/dispatcher.h [new file with mode: 0644]
src/gui/drawing.cpp [new file with mode: 0644]
src/gui/drawing.h [new file with mode: 0644]
src/gui/elems/actionEditor/baseAction.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/baseAction.h [new file with mode: 0644]
src/gui/elems/actionEditor/baseActionEditor.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/baseActionEditor.h [new file with mode: 0644]
src/gui/elems/actionEditor/envelopeEditor.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/envelopeEditor.h [new file with mode: 0644]
src/gui/elems/actionEditor/envelopePoint.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/envelopePoint.h [new file with mode: 0644]
src/gui/elems/actionEditor/gridTool.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/gridTool.h [new file with mode: 0644]
src/gui/elems/actionEditor/pianoItem.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/pianoItem.h [new file with mode: 0644]
src/gui/elems/actionEditor/pianoRoll.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/pianoRoll.h [new file with mode: 0644]
src/gui/elems/actionEditor/sampleAction.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/sampleAction.h [new file with mode: 0644]
src/gui/elems/actionEditor/sampleActionEditor.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/sampleActionEditor.h [new file with mode: 0644]
src/gui/elems/actionEditor/splitScroll.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/splitScroll.h [new file with mode: 0644]
src/gui/elems/actionEditor/velocityEditor.cpp [new file with mode: 0644]
src/gui/elems/actionEditor/velocityEditor.h [new file with mode: 0644]
src/gui/elems/basics/box.cpp [new file with mode: 0644]
src/gui/elems/basics/box.h [new file with mode: 0644]
src/gui/elems/basics/boxtypes.cpp [new file with mode: 0644]
src/gui/elems/basics/boxtypes.h [new file with mode: 0644]
src/gui/elems/basics/browser.cpp [new file with mode: 0644]
src/gui/elems/basics/browser.h [new file with mode: 0644]
src/gui/elems/basics/button.cpp [new file with mode: 0644]
src/gui/elems/basics/button.h [new file with mode: 0644]
src/gui/elems/basics/check.cpp [new file with mode: 0644]
src/gui/elems/basics/check.h [new file with mode: 0644]
src/gui/elems/basics/choice.cpp [new file with mode: 0644]
src/gui/elems/basics/choice.h [new file with mode: 0644]
src/gui/elems/basics/dial.cpp [new file with mode: 0644]
src/gui/elems/basics/dial.h [new file with mode: 0644]
src/gui/elems/basics/flex.cpp [new file with mode: 0644]
src/gui/elems/basics/flex.h [new file with mode: 0644]
src/gui/elems/basics/group.cpp [new file with mode: 0644]
src/gui/elems/basics/group.h [new file with mode: 0644]
src/gui/elems/basics/input.cpp [new file with mode: 0644]
src/gui/elems/basics/input.h [new file with mode: 0644]
src/gui/elems/basics/liquidScroll.cpp [new file with mode: 0644]
src/gui/elems/basics/liquidScroll.h [new file with mode: 0644]
src/gui/elems/basics/pack.cpp [new file with mode: 0644]
src/gui/elems/basics/pack.h [new file with mode: 0644]
src/gui/elems/basics/progress.cpp [new file with mode: 0644]
src/gui/elems/basics/progress.h [new file with mode: 0644]
src/gui/elems/basics/resizerBar.cpp [new file with mode: 0644]
src/gui/elems/basics/resizerBar.h [new file with mode: 0644]
src/gui/elems/basics/scroll.cpp [new file with mode: 0644]
src/gui/elems/basics/scroll.h [new file with mode: 0644]
src/gui/elems/basics/scrollPack.cpp [new file with mode: 0644]
src/gui/elems/basics/scrollPack.h [new file with mode: 0644]
src/gui/elems/basics/slider.cpp [new file with mode: 0644]
src/gui/elems/basics/slider.h [new file with mode: 0644]
src/gui/elems/basics/split.cpp [new file with mode: 0644]
src/gui/elems/basics/split.h [new file with mode: 0644]
src/gui/elems/basics/statusButton.cpp [new file with mode: 0644]
src/gui/elems/basics/statusButton.h [new file with mode: 0644]
src/gui/elems/basics/tabs.cpp [new file with mode: 0644]
src/gui/elems/basics/tabs.h [new file with mode: 0644]
src/gui/elems/config/tabAudio.cpp [new file with mode: 0644]
src/gui/elems/config/tabAudio.h [new file with mode: 0644]
src/gui/elems/config/tabBehaviors.cpp [new file with mode: 0644]
src/gui/elems/config/tabBehaviors.h [new file with mode: 0644]
src/gui/elems/config/tabBindings.cpp [new file with mode: 0644]
src/gui/elems/config/tabBindings.h [new file with mode: 0644]
src/gui/elems/config/tabMidi.cpp [new file with mode: 0644]
src/gui/elems/config/tabMidi.h [new file with mode: 0644]
src/gui/elems/config/tabMisc.cpp [new file with mode: 0644]
src/gui/elems/config/tabMisc.h [new file with mode: 0644]
src/gui/elems/config/tabPlugins.cpp [new file with mode: 0644]
src/gui/elems/config/tabPlugins.h [new file with mode: 0644]
src/gui/elems/fileBrowser.cpp [new file with mode: 0644]
src/gui/elems/fileBrowser.h [new file with mode: 0644]
src/gui/elems/keyBinder.cpp [new file with mode: 0644]
src/gui/elems/keyBinder.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/channel.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/channel.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/channelButton.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/channelButton.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/channelMode.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/channelMode.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/channelStatus.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/channelStatus.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/column.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/column.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/keyboard.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/keyboard.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/midiActivity.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/midiActivity.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/midiChannel.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/midiChannel.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/midiChannelButton.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/midiChannelButton.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/sampleChannel.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/sampleChannel.h [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/sampleChannelButton.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/keyboard/sampleChannelButton.h [new file with mode: 0644]
src/gui/elems/mainWindow/mainIO.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/mainIO.h [new file with mode: 0644]
src/gui/elems/mainWindow/mainMenu.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/mainMenu.h [new file with mode: 0644]
src/gui/elems/mainWindow/mainTimer.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/mainTimer.h [new file with mode: 0644]
src/gui/elems/mainWindow/mainTransport.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/mainTransport.h [new file with mode: 0644]
src/gui/elems/mainWindow/sequencer.cpp [new file with mode: 0644]
src/gui/elems/mainWindow/sequencer.h [new file with mode: 0644]
src/gui/elems/midiIO/midiLearner.cpp [new file with mode: 0644]
src/gui/elems/midiIO/midiLearner.h [new file with mode: 0644]
src/gui/elems/midiIO/midiLearnerPack.cpp [new file with mode: 0644]
src/gui/elems/midiIO/midiLearnerPack.h [new file with mode: 0644]
src/gui/elems/plugin/pluginBrowser.cpp [new file with mode: 0644]
src/gui/elems/plugin/pluginBrowser.h [new file with mode: 0644]
src/gui/elems/plugin/pluginElement.cpp [new file with mode: 0644]
src/gui/elems/plugin/pluginElement.h [new file with mode: 0644]
src/gui/elems/plugin/pluginParameter.cpp [new file with mode: 0644]
src/gui/elems/plugin/pluginParameter.h [new file with mode: 0644]
src/gui/elems/sampleEditor/boostTool.cpp [new file with mode: 0644]
src/gui/elems/sampleEditor/boostTool.h [new file with mode: 0644]
src/gui/elems/sampleEditor/panTool.cpp [new file with mode: 0644]
src/gui/elems/sampleEditor/panTool.h [new file with mode: 0644]
src/gui/elems/sampleEditor/pitchTool.cpp [new file with mode: 0644]
src/gui/elems/sampleEditor/pitchTool.h [new file with mode: 0644]
src/gui/elems/sampleEditor/rangeTool.cpp [new file with mode: 0644]
src/gui/elems/sampleEditor/rangeTool.h [new file with mode: 0644]
src/gui/elems/sampleEditor/shiftTool.cpp [new file with mode: 0644]
src/gui/elems/sampleEditor/shiftTool.h [new file with mode: 0644]
src/gui/elems/sampleEditor/volumeTool.cpp [new file with mode: 0644]
src/gui/elems/sampleEditor/volumeTool.h [new file with mode: 0644]
src/gui/elems/sampleEditor/waveTools.cpp [new file with mode: 0644]
src/gui/elems/sampleEditor/waveTools.h [new file with mode: 0644]
src/gui/elems/sampleEditor/waveform.cpp [new file with mode: 0644]
src/gui/elems/sampleEditor/waveform.h [new file with mode: 0644]
src/gui/elems/soundMeter.cpp [new file with mode: 0644]
src/gui/elems/soundMeter.h [new file with mode: 0644]
src/gui/types.h [new file with mode: 0644]
src/gui/ui.cpp [new file with mode: 0644]
src/gui/ui.h [new file with mode: 0644]
src/gui/updater.cpp [new file with mode: 0644]
src/gui/updater.h [new file with mode: 0644]
src/main.cpp [new file with mode: 0644]
src/utils/cocoa.h [new file with mode: 0644]
src/utils/cocoa.mm [new file with mode: 0644]
src/utils/fs.cpp [new file with mode: 0644]
src/utils/fs.h [new file with mode: 0644]
src/utils/gui.cpp [new file with mode: 0644]
src/utils/gui.h [new file with mode: 0644]
src/utils/log.cpp [new file with mode: 0644]
src/utils/log.h [new file with mode: 0644]
src/utils/math.cpp [new file with mode: 0644]
src/utils/math.h [new file with mode: 0644]
src/utils/string.cpp [new file with mode: 0644]
src/utils/string.h [new file with mode: 0644]
src/utils/time.cpp [new file with mode: 0644]
src/utils/time.h [new file with mode: 0644]
src/utils/vector.h [new file with mode: 0644]
src/utils/ver.cpp [new file with mode: 0644]
src/utils/ver.h [new file with mode: 0644]
tests/actionRecorder.cpp [new file with mode: 0644]
tests/main.cpp [new file with mode: 0644]
tests/midiLighter.cpp [new file with mode: 0644]
tests/mocks/kernelMidiMock.h [new file with mode: 0644]
tests/resources/test.wav [new file with mode: 0644]
tests/samplePlayer.cpp [new file with mode: 0644]
tests/utils.cpp [new file with mode: 0644]
tests/wave.cpp [new file with mode: 0644]
tests/waveFx.cpp [new file with mode: 0644]
tests/waveManager.cpp [new file with mode: 0644]
tests/waveReader.cpp [new file with mode: 0644]

diff --git a/.clang-format b/.clang-format
new file mode 100644 (file)
index 0000000..463096b
--- /dev/null
@@ -0,0 +1,16 @@
+---
+BasedOnStyle: Microsoft
+AccessModifierOffset: -4
+AlignAfterOpenBracket: 'false'
+AlignConsecutiveAssignments: 'true'
+AlignConsecutiveDeclarations: 'true'
+AllowShortFunctionsOnASingleLine: All
+BreakBeforeBraces: Allman
+BreakConstructorInitializers: BeforeComma
+ColumnLimit: 0
+ConstructorInitializerIndentWidth: '0'
+IndentWrappedFunctionNames: 'false'
+Language: Cpp
+NamespaceIndentation: None
+PointerAlignment: Left
+UseTab: ForIndentation
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5ec69d1
--- /dev/null
@@ -0,0 +1,593 @@
+# ------------------------------------------------------------------------------
+# Preliminary setup
+# ------------------------------------------------------------------------------
+
+cmake_minimum_required(VERSION 3.12)
+
+# CMAKE_OSX_DEPLOYMENT_TARGET should be set prior to the first project() or
+# enable_language() command invocation because it may influence configuration
+# of the toolchain and flags.
+# Also, see https://stackoverflow.com/questions/34208360/cmake-seems-to-ignore-cmake-osx-deployment-target
+
+if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
+       set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14" CACHE STRING "Minimum OS X deployment version")
+endif()
+
+# ------------------------------------------------------------------------------
+# Project
+# ------------------------------------------------------------------------------
+
+project(giada LANGUAGES CXX)
+
+# ------------------------------------------------------------------------------
+# Lists definition
+#
+# SOURCES - contains the source files
+# PREPROCESSOR_DEFS - preprocessor definitions
+# INCLUDE_DIRS - include directories (e.g. -I)
+# COMPILER_OPTIONS - additional flags for the compiler
+# LIBRARIES - external dependencies to link
+# COMPILER_FEATURES - e.g. C++17
+# TARGET_PROPERTIES - additional properties for the target 'giada'.
+# ------------------------------------------------------------------------------
+
+list(APPEND SOURCES
+       src/main.cpp
+       src/core/engine.cpp
+       src/core/worker.cpp
+       src/core/eventDispatcher.cpp
+       src/core/midiDispatcher.cpp
+       src/core/midiMapper.cpp
+       src/core/midiEvent.cpp
+       src/core/quantizer.cpp
+       src/core/conf.cpp
+       src/core/kernelAudio.cpp
+       src/core/jackTransport.cpp
+       src/core/mixerHandler.cpp
+       src/core/sequencer.cpp
+       src/core/metronome.cpp
+       src/core/init.cpp
+       src/core/wave.cpp
+       src/core/waveFx.cpp
+       src/core/kernelMidi.cpp
+       src/core/graphics.cpp
+       src/core/patch.cpp
+       src/core/actions/actionRecorder.cpp
+       src/core/actions/actions.cpp
+       src/core/mixer.cpp
+    src/core/synchronizer.cpp
+       src/core/waveManager.cpp
+    src/core/recorder.cpp
+       src/core/midiLearnParam.cpp
+       src/core/resampler.cpp
+       src/core/plugins/pluginHost.cpp
+       src/core/plugins/pluginManager.cpp
+       src/core/plugins/plugin.cpp
+       src/core/plugins/pluginState.cpp
+       src/core/channels/sampleActionRecorder.cpp
+       src/core/channels/midiActionRecorder.cpp
+       src/core/channels/waveReader.cpp
+       src/core/channels/midiController.cpp
+       src/core/channels/sampleReactor.cpp
+       src/core/channels/sampleAdvancer.cpp
+       src/core/channels/samplePlayer.cpp
+       src/core/channels/audioReceiver.cpp
+       src/core/channels/midiLighter.cpp
+       src/core/channels/midiLearner.cpp
+       src/core/channels/midiSender.cpp
+       src/core/channels/midiReceiver.cpp
+       src/core/channels/channel.cpp
+       src/core/channels/channelManager.cpp
+       src/core/model/sequencer.cpp
+       src/core/model/mixer.cpp
+       src/core/model/recorder.cpp
+       src/core/model/model.cpp
+       src/core/model/storage.cpp
+       src/core/idManager.cpp
+       src/glue/events.cpp
+       src/glue/main.cpp
+       src/glue/io.cpp
+       src/glue/storage.cpp
+       src/glue/channel.cpp
+       src/glue/plugin.cpp
+       src/glue/recorder.cpp
+       src/glue/sampleEditor.cpp
+       src/glue/actionEditor.cpp
+       src/glue/config.cpp
+       src/glue/layout.cpp
+       src/gui/ui.cpp
+       src/gui/dialogs/window.cpp
+       src/gui/dispatcher.cpp
+       src/gui/updater.cpp
+       src/gui/drawing.cpp
+       src/gui/dialogs/progress.cpp
+       src/gui/dialogs/keyGrabber.cpp
+       src/gui/dialogs/about.cpp
+       src/gui/dialogs/mainWindow.cpp
+       src/gui/dialogs/beatsInput.cpp
+       src/gui/dialogs/warnings.cpp
+       src/gui/dialogs/bpmInput.cpp
+       src/gui/dialogs/channelNameInput.cpp
+       src/gui/dialogs/config.cpp
+       src/gui/dialogs/pluginList.cpp
+       src/gui/dialogs/pluginWindow.cpp
+       src/gui/dialogs/sampleEditor.cpp
+       src/gui/dialogs/pluginWindowGUI.cpp
+       src/gui/dialogs/pluginChooser.cpp
+       src/gui/dialogs/missingAssets.cpp
+       src/gui/dialogs/actionEditor/baseActionEditor.cpp
+       src/gui/dialogs/actionEditor/sampleActionEditor.cpp
+       src/gui/dialogs/actionEditor/midiActionEditor.cpp
+       src/gui/dialogs/browser/browserBase.cpp
+       src/gui/dialogs/browser/browserDir.cpp
+       src/gui/dialogs/browser/browserLoad.cpp
+       src/gui/dialogs/browser/browserSave.cpp
+       src/gui/dialogs/midiIO/midiOutputBase.cpp
+       src/gui/dialogs/midiIO/midiOutputSampleCh.cpp
+       src/gui/dialogs/midiIO/midiOutputMidiCh.cpp
+       src/gui/dialogs/midiIO/midiInputBase.cpp
+       src/gui/dialogs/midiIO/midiInputChannel.cpp
+       src/gui/dialogs/midiIO/midiInputMaster.cpp
+       src/gui/elems/midiIO/midiLearner.cpp
+       src/gui/elems/midiIO/midiLearnerPack.cpp
+    src/gui/elems/fileBrowser.cpp
+       src/gui/elems/soundMeter.cpp
+       src/gui/elems/keyBinder.cpp
+       src/gui/elems/plugin/pluginBrowser.cpp
+       src/gui/elems/plugin/pluginParameter.cpp
+       src/gui/elems/plugin/pluginElement.cpp
+       src/gui/elems/sampleEditor/waveform.cpp
+       src/gui/elems/sampleEditor/waveTools.cpp
+       src/gui/elems/sampleEditor/volumeTool.cpp
+       src/gui/elems/sampleEditor/boostTool.cpp
+       src/gui/elems/sampleEditor/panTool.cpp
+       src/gui/elems/sampleEditor/pitchTool.cpp
+       src/gui/elems/sampleEditor/rangeTool.cpp
+       src/gui/elems/sampleEditor/shiftTool.cpp
+       src/gui/elems/actionEditor/baseActionEditor.cpp
+       src/gui/elems/actionEditor/baseAction.cpp
+       src/gui/elems/actionEditor/envelopeEditor.cpp
+       src/gui/elems/actionEditor/velocityEditor.cpp
+       src/gui/elems/actionEditor/envelopePoint.cpp
+       src/gui/elems/actionEditor/pianoRoll.cpp
+       src/gui/elems/actionEditor/pianoItem.cpp
+       src/gui/elems/actionEditor/sampleActionEditor.cpp
+       src/gui/elems/actionEditor/sampleAction.cpp
+       src/gui/elems/actionEditor/gridTool.cpp
+       src/gui/elems/actionEditor/splitScroll.cpp
+       src/gui/elems/mainWindow/mainIO.cpp
+       src/gui/elems/mainWindow/mainMenu.cpp
+       src/gui/elems/mainWindow/mainTimer.cpp
+       src/gui/elems/mainWindow/mainTransport.cpp
+       src/gui/elems/mainWindow/sequencer.cpp
+       src/gui/elems/mainWindow/keyboard/channelMode.cpp
+       src/gui/elems/mainWindow/keyboard/channelButton.cpp
+       src/gui/elems/mainWindow/keyboard/channelStatus.cpp
+       src/gui/elems/mainWindow/keyboard/keyboard.cpp
+       src/gui/elems/mainWindow/keyboard/column.cpp
+       src/gui/elems/mainWindow/keyboard/sampleChannel.cpp
+       src/gui/elems/mainWindow/keyboard/midiChannel.cpp
+       src/gui/elems/mainWindow/keyboard/channel.cpp
+       src/gui/elems/mainWindow/keyboard/sampleChannelButton.cpp
+       src/gui/elems/mainWindow/keyboard/midiChannelButton.cpp
+       src/gui/elems/mainWindow/keyboard/midiActivity.cpp
+       src/gui/elems/config/tabMisc.cpp
+       src/gui/elems/config/tabMidi.cpp
+       src/gui/elems/config/tabAudio.cpp
+       src/gui/elems/config/tabBehaviors.cpp
+       src/gui/elems/config/tabPlugins.cpp
+       src/gui/elems/config/tabBindings.cpp
+       src/gui/elems/basics/scroll.cpp
+       src/gui/elems/basics/pack.cpp
+       src/gui/elems/basics/group.cpp
+       src/gui/elems/basics/scrollPack.cpp
+       src/gui/elems/basics/boxtypes.cpp
+       src/gui/elems/basics/statusButton.cpp
+       src/gui/elems/basics/button.cpp
+       src/gui/elems/basics/resizerBar.cpp
+       src/gui/elems/basics/input.cpp
+       src/gui/elems/basics/liquidScroll.cpp
+       src/gui/elems/basics/choice.cpp
+       src/gui/elems/basics/dial.cpp
+       src/gui/elems/basics/box.cpp
+       src/gui/elems/basics/slider.cpp
+       src/gui/elems/basics/progress.cpp
+       src/gui/elems/basics/check.cpp
+       src/gui/elems/basics/split.cpp
+       src/gui/elems/basics/browser.cpp
+       src/gui/elems/basics/flex.cpp
+       src/gui/elems/basics/tabs.cpp
+       src/utils/log.cpp
+       src/utils/time.cpp
+       src/utils/math.cpp
+       src/utils/gui.cpp
+       src/utils/fs.cpp
+       src/utils/ver.cpp
+       src/utils/string.cpp
+       src/deps/rtaudio/RtAudio.cpp
+       src/deps/mcl-audio-buffer/src/audioBuffer.cpp)
+
+list(APPEND PREPROCESSOR_DEFS)
+list(APPEND INCLUDE_DIRS
+       ${CMAKE_SOURCE_DIR}
+       ${CMAKE_SOURCE_DIR}/src)
+list(APPEND COMPILER_OPTIONS)
+list(APPEND LIBRARIES)
+list(APPEND COMPILER_FEATURES cxx_std_17)
+list(APPEND TARGET_PROPERTIES)
+
+# ------------------------------------------------------------------------------
+# Detect OS
+# ------------------------------------------------------------------------------
+
+if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+       set(OS_LINUX 1)
+elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+       set(OS_WINDOWS 1)
+elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
+       set(OS_MACOS 1)
+elseif(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
+       set(OS_FREEBSD 1)
+else()
+       message(FATAL_ERROR "Unsupported platform '${CMAKE_SYSTEM_NAME}', quitting.")
+endif()
+
+# ------------------------------------------------------------------------------
+# Compiler warnings
+# ------------------------------------------------------------------------------
+
+if(DEFINED OS_WINDOWS)
+       list(APPEND COMPILER_OPTIONS /W4 /bigobj /external:anglebrackets /external:W0)
+else()
+       list(APPEND COMPILER_OPTIONS -Wall -Wextra -Wpedantic)
+endif()
+
+# ------------------------------------------------------------------------------
+# Options
+# ------------------------------------------------------------------------------
+
+option(WITH_VST2 "Enable VST2 support." OFF)
+option(WITH_VST3 "Enable VST3 support." OFF)
+option(WITH_TESTS "Include the test suite." OFF)
+
+if(DEFINED OS_LINUX)
+       option(WITH_ALSA "Enable ALSA support (Linux only)." ON)
+       option(WITH_PULSE "Enable PulseAudio support (Linux only)." ON)
+       option(WITH_JACK "Enable JACK support (Linux only)." ON)
+endif()
+
+if(WITH_TESTS)
+       list(APPEND PREPROCESSOR_DEFS 
+               WITH_TESTS
+               TEST_RESOURCES_DIR="${CMAKE_SOURCE_DIR}/tests/resources/")
+endif()
+
+if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
+       list(APPEND PREPROCESSOR_DEFS NDEBUG)
+endif()
+
+# ------------------------------------------------------------------------------
+# Dependencies
+# ------------------------------------------------------------------------------
+
+# Threads (system)
+
+set(THREADS_PREFER_PTHREAD_FLAG ON)
+find_package(Threads REQUIRED)
+list(APPEND LIBRARIES Threads::Threads)
+
+# pkg-config/pkgconf, required to find some external dependencies on some
+# platforms
+find_package(PkgConfig)
+
+# RtMidi
+
+find_package(RtMidi CONFIG)
+if (RtMidi_FOUND)
+       list(APPEND LIBRARIES RtMidi::rtmidi)
+       message("RtMidi library found in " ${RtMidi_DIR})
+elseif (PkgConfig_FOUND)
+       pkg_check_modules(RtMidi IMPORTED_TARGET rtmidi)
+       if (RtMidi_FOUND)
+               list(APPEND LIBRARIES PkgConfig::RtMidi)
+               message("RtMidi library found")
+       endif()
+endif()
+
+if (NOT RtMidi_FOUND)
+       # Fallback to find_library mode (in case rtmidi is too old). 
+       find_library(LIBRARY_RTMIDI NAMES rtmidi)
+       list(APPEND LIBRARIES ${LIBRARY_RTMIDI})
+
+       if (NOT LIBRARY_RTMIDI)
+               message(FATAL_ERROR "Can't find RtMidi, aborting.")
+       endif()
+
+       # RtMidi header path may vary across OSes, so a fix is needed.
+       # TODO - Find a way to avoid this additional step
+
+       find_path(LIBRARY_RTMIDI_INCLUDE_DIR RtMidi.h PATH_SUFFIXES rtmidi)
+       list(APPEND INCLUDE_DIRS ${LIBRARY_RTMIDI_INCLUDE_DIR})
+       message("RtMidi library found in " ${RtMidi_DIR})
+endif()
+
+# FLTK 
+
+set(FLTK_SKIP_FLUID TRUE)  # Don't search for FLTK's fluid
+set(FLTK_SKIP_OPENGL TRUE) # Don't search for FLTK's OpenGL
+find_package(FLTK REQUIRED NO_MODULE)
+list(APPEND LIBRARIES fltk)
+list(APPEND INCLUDE_DIRS ${FLTK_INCLUDE_DIRS})
+message("FLTK library found in " ${FLTK_DIR})
+
+# Libsndfile
+
+find_package(SndFile CONFIG)
+if (SndFile_FOUND)
+       list(APPEND LIBRARIES SndFile::sndfile)
+       message("Libsndfile library found in " ${SndFile_DIR})
+elseif(PkgConfig_FOUND)
+       pkg_check_modules(SndFile IMPORTED_TARGET sndfile)
+       if (SndFile_FOUND)
+               list(APPEND LIBRARIES PkgConfig::SndFile)
+               message("Libsndfile library found")
+       endif() 
+endif()
+
+if (NOT SndFile_FOUND)
+       # Fallback to find_library mode (in case libsndfile is too old). 
+       find_library(LIBRARY_SNDFILE NAMES sndfile libsndfile libsndfile-1)
+       
+       if (NOT LIBRARY_SNDFILE)
+               message(FATAL_ERROR "Can't find libsndfile, aborting.")
+       endif()
+
+       list(APPEND LIBRARIES ${LIBRARY_SNDFILE})
+       message("Libsndfile library found in " ${LIBRARY_SNDFILE})
+
+       # Find optional dependencies.
+
+       find_library(LIBRARY_FLAC NAMES flac FLAC)
+       find_library(LIBRARY_OGG NAMES ogg)
+       find_library(LIBRARY_OPUS NAMES opus libopus)
+       find_library(LIBRARY_VORBIS NAMES vorbis)
+       find_library(LIBRARY_VORBISENC NAMES vorbisenc)
+
+       if(LIBRARY_FLAC)
+               list(APPEND LIBRARIES ${LIBRARY_FLAC})
+       endif()
+       if(LIBRARY_OGG)
+               list(APPEND LIBRARIES ${LIBRARY_OGG})
+       endif()
+       if(LIBRARY_OPUS)
+               list(APPEND LIBRARIES ${LIBRARY_OPUS})
+       endif()
+       if(LIBRARY_VORBIS)
+               list(APPEND LIBRARIES ${LIBRARY_VORBIS})
+       endif()
+       if(LIBRARY_VORBISENC)
+               list(APPEND LIBRARIES ${LIBRARY_VORBISENC})
+       endif() 
+endif()
+
+# Libsamplerate
+
+find_package(SampleRate CONFIG)
+if (SampleRate_FOUND)
+       list(APPEND LIBRARIES SampleRate::samplerate)
+       message("Libsamplerate library found in " ${SampleRate_DIR})
+else() 
+       # Fallback to find_library mode (in case Libsamplerate is too old). 
+       find_library(LIBRARY_SAMPLERATE 
+           NAMES samplerate libsamplerate libsamplerate-0 liblibsamplerate-0
+           PATHS ${_VCPKG_ROOT_DIR}/installed/${VCPKG_TARGET_TRIPLET}
+               REQUIRED)
+       list(APPEND LIBRARIES ${LIBRARY_SAMPLERATE})
+       message("Libsamplerate library found in " ${LIBRARY_SAMPLERATE})
+endif()
+
+# Catch (if tests enabled)
+
+if (WITH_TESTS)
+
+       find_package(Catch2 CONFIG REQUIRED)
+       list(APPEND LIBRARIES Catch2::Catch2)
+       message("Catch2 library found in " ${Catch2_DIR})
+
+endif()
+
+# ------------------------------------------------------------------------------
+# Conditional checks for different platforms.
+# ------------------------------------------------------------------------------
+
+if(DEFINED OS_LINUX)
+
+       find_package(X11 REQUIRED)
+       find_package(ALSA REQUIRED)
+       find_library(LIBRARY_PULSE NAMES pulse REQUIRED)
+       find_library(LIBRARY_PULSE_SIMPLE NAMES pulse-simple REQUIRED)
+       find_library(LIBRARY_FONTCONFIG NAMES fontconfig REQUIRED)
+       pkg_check_modules(JACK REQUIRED jack)
+       list(APPEND LIBRARIES
+               ${X11_LIBRARIES} ${X11_Xrender_LIB} ${X11_Xft_LIB} ${X11_Xfixes_LIB}
+               ${X11_Xinerama_LIB} ${X11_Xcursor_LIB} ${X11_Xpm_LIB} ${LIBRARY_PULSE}
+               ${LIBRARY_PULSE_SIMPLE} ${LIBRARY_FONTCONFIG} ${JACK_LDFLAGS}
+               ${CMAKE_DL_LIBS} ${ALSA_LIBRARIES} pthread stdc++fs)
+
+       if (WITH_ALSA)
+               list(APPEND PREPROCESSOR_DEFS __LINUX_ALSA__)
+       endif()
+       if (WITH_PULSE)
+               list(APPEND PREPROCESSOR_DEFS __LINUX_PULSE__)
+       endif()
+       if (WITH_JACK)
+               list(APPEND PREPROCESSOR_DEFS WITH_AUDIO_JACK __UNIX_JACK__)
+       endif()
+
+elseif(DEFINED OS_WINDOWS)
+
+       list(APPEND LIBRARIES dsound)
+
+       list(APPEND SOURCES
+               src/deps/rtaudio/include/asio.h
+               src/deps/rtaudio/include/asio.cpp
+               src/deps/rtaudio/include/asiosys.h
+               src/deps/rtaudio/include/asiolist.h
+               src/deps/rtaudio/include/asiolist.cpp
+               src/deps/rtaudio/include/asiodrivers.h
+               src/deps/rtaudio/include/asiodrivers.cpp
+               src/deps/rtaudio/include/iasiothiscallresolver.h
+               src/deps/rtaudio/include/iasiothiscallresolver.cpp
+               src/ext/resource.rc)
+
+       list(APPEND INCLUDE_DIRS
+               src/deps/rtaudio/include)
+
+       list(APPEND PREPROCESSOR_DEFS
+               __WINDOWS_ASIO__
+               __WINDOWS_WASAPI__
+               __WINDOWS_DS__)
+
+elseif(DEFINED OS_MACOS)
+
+       find_library(CORE_AUDIO_LIBRARY CoreAudio REQUIRED)
+       find_library(CORE_MIDI_LIBRARY CoreMIDI REQUIRED)
+       find_library(COCOA_LIBRARY Cocoa REQUIRED)
+       find_library(CARBON_LIBRARY Carbon REQUIRED)
+       find_library(CORE_FOUNDATION_LIBRARY CoreFoundation REQUIRED)
+       find_library(ACCELERATE_LIBRARY Accelerate REQUIRED)
+       find_library(WEBKIT_LIBRARY WebKit REQUIRED)
+       find_library(QUARZ_CORE_LIBRARY QuartzCore REQUIRED)
+       find_library(IOKIT_LIBRARY IOKit REQUIRED)
+       list(APPEND LIBRARIES
+               ${CORE_AUDIO_LIBRARY} ${CORE_MIDI_LIBRARY} ${COCOA_LIBRARY}
+               ${CARBON_LIBRARY} ${CORE_FOUNDATION_LIBRARY} ${ACCELERATE_LIBRARY}
+               ${WEBKIT_LIBRARY} ${QUARZ_CORE_LIBRARY} ${IOKIT_LIBRARY})
+
+       list(APPEND SOURCES
+               src/utils/cocoa.mm
+               src/utils/cocoa.h)
+
+       # TODO: why??
+       list(APPEND INCLUDE_DIRS
+               "/usr/local/include")
+
+       list(APPEND PREPROCESSOR_DEFS 
+               __MACOSX_CORE__)
+
+elseif (DEFINED OS_FREEBSD)
+
+       find_package(X11 REQUIRED)
+       find_library(LIBRARY_PULSE NAMES pulse REQUIRED)
+       find_library(LIBRARY_PULSE_SIMPLE NAMES pulse-simple REQUIRED)
+       find_library(LIBRARY_FONTCONFIG NAMES fontconfig REQUIRED)
+       find_library(LIBRARY_JACK NAMES jack REQUIRED)
+       list(APPEND LIBRARIES
+               ${X11_LIBRARIES} ${X11_Xrender_LIB} ${X11_Xft_LIB} ${X11_Xfixes_LIB}
+               ${X11_Xinerama_LIB} ${X11_Xcursor_LIB} ${X11_Xpm_LIB} ${LIBRARY_PULSE}
+               ${LIBRARY_PULSE_SIMPLE} ${LIBRARY_FONTCONFIG} ${LIBRARY_JACK}
+               ${CMAKE_DL_LIBS} pthread)
+       
+       list(APPEND PREPROCESSOR_DEFS
+               WITH_AUDIO_JACK
+               __LINUX_PULSE__
+               __UNIX_JACK__)
+
+endif()
+
+# ------------------------------------------------------------------------------
+# Extra parameters if compiled with VST.
+# ------------------------------------------------------------------------------
+
+if(WITH_VST2 OR WITH_VST3)
+
+       list(APPEND SOURCES
+               src/deps/juce/modules/juce_audio_basics/juce_audio_basics.cpp
+               src/deps/juce/modules/juce_audio_processors/juce_audio_processors.cpp
+               src/deps/juce/modules/juce_core/juce_core.cpp
+               src/deps/juce/modules/juce_data_structures/juce_data_structures.cpp
+               src/deps/juce/modules/juce_events/juce_events.cpp
+               src/deps/juce/modules/juce_graphics/juce_graphics.cpp
+               src/deps/juce/modules/juce_gui_basics/juce_gui_basics.cpp
+               src/deps/juce/modules/juce_gui_extra/juce_gui_extra.cpp)
+
+       list(APPEND INCLUDE_DIRS
+               ${CMAKE_SOURCE_DIR}/src/deps/juce/modules
+               ${CMAKE_SOURCE_DIR}/src/deps/vst3sdk)
+
+       if(DEFINED OS_LINUX)
+               find_package(Freetype REQUIRED)
+               list(APPEND LIBRARIES ${FREETYPE_LIBRARIES})
+               list(APPEND INCLUDE_DIRS ${FREETYPE_INCLUDE_DIRS})
+       endif()
+
+       list(APPEND PREPROCESSOR_DEFS
+               WITH_VST
+               JUCE_DEBUG=$<BOOL:$<CONFIG:Debug>>
+               JUCE_MODAL_LOOPS_PERMITTED=1
+               JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1
+               JUCE_MODULE_AVAILABLE_juce_gui_basics=1
+               JUCE_STANDALONE_APPLICATION=1
+               JUCE_PLUGINHOST_AU=0
+               JUCE_WEB_BROWSER=0
+               JUCE_USE_CURL=0)
+
+       if(WITH_VST2)
+               list(APPEND PREPROCESSOR_DEFS
+                       WITH_VST2
+                       JUCE_PLUGINHOST_VST=1)
+       endif()
+       if(WITH_VST3)
+               list(APPEND PREPROCESSOR_DEFS
+                       WITH_VST3
+                       JUCE_PLUGINHOST_VST3=1)
+       endif()
+
+endif()
+
+# ------------------------------------------------------------------------------
+# Finalize 'giada' target (main executable).
+# ------------------------------------------------------------------------------
+
+add_executable(giada)
+target_compile_features(giada PRIVATE ${COMPILER_FEATURES})
+target_sources(giada PRIVATE ${SOURCES})
+target_compile_definitions(giada PRIVATE ${PREPROCESSOR_DEFS})
+target_include_directories(giada PRIVATE ${INCLUDE_DIRS})
+target_link_libraries(giada PRIVATE ${LIBRARIES})
+target_compile_options(giada PRIVATE ${COMPILER_OPTIONS})
+
+# ------------------------------------------------------------------------------
+# Install rules
+# ------------------------------------------------------------------------------
+
+if(DEFINED OS_LINUX)
+       include(GNUInstallDirs)
+       install(TARGETS giada DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR})
+       install(FILES ${CMAKE_SOURCE_DIR}/extras/com.giadamusic.Giada.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications)
+       install(FILES ${CMAKE_SOURCE_DIR}/extras/com.giadamusic.Giada.metainfo.xml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/metainfo)
+       install(FILES ${CMAKE_SOURCE_DIR}/extras/giada-logo.svg RENAME com.giadamusic.Giada.svg DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/apps)
+endif()
+
+# ------------------------------------------------------------------------------
+# Extra
+# ------------------------------------------------------------------------------
+
+# TODO - move these into the 'if [OS]' conditionals (needs smarter list first)
+
+if(DEFINED OS_WINDOWS)
+
+       # Enable static linking of the MSVC runtime library on Windows
+       
+       set_target_properties(giada PROPERTIES
+               MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
+
+elseif(DEFINED OS_MACOS)
+
+       # Enable hardened runtime:
+       # https://developer.apple.com/documentation/security/hardened_runtime
+       
+       set_target_properties(giada PROPERTIES
+               XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES)
+
+endif()
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..77bd592
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,1391 @@
+--------------------------------------------------------------------------------
+
+
+  Giada - Your Hardcore Loopmachine.
+
+  Developed by Monocasual Laboratories
+
+  www.giadamusic.com
+
+  CHANGELOG
+--------------------------------------------------------------------------------
+
+
+0.20.1 --- 2022 . 02 . 21
+- New MIDI I/O activity LEDs on channels (#143)
+- New "Missing Assets" alert window (#344)
+- Many smaller improvements and cleanups in UI code
+- Add ability to sort installed plug-ins by Format (VST, VST3, ...)
+- Update JUCE to 6.1.5
+- Update custom RtAudio submodule (now pointing to 6.0.0beta1)
+- Optimize internal buffer Giada <-> JUCE conversion
+- Remove old plug-in parameter storage used in old patches
+- Fix deadlock when using JACK transport
+- Fix Action Editor grid refresh when changing BPM while the editor window is open (#547)
+- Fix plug-in clone operation while cloning a channel (#551)
+
+
+0.20.0 --- 2022 . 01 . 24
+- Show progress bar for long operations 
+- Improved rendering algorithm for sample channels
+- Fix wrong sample tail rendering when pitch != 1.0 
+- Always display play head in Action Editor (fix #534)
+- Fix re-initialization order of engine sub-components (fixes #533)
+- Change 'kill chan' wording to 'stop note' in Action Editor (fixes #532)
+- Update solo count when deleting a channel (fixes #540)
+- Update Main Window title saving a new project (fixes #541)
+- [Config] Don't skip MIDI device fetching if one of the ports fail to open
+- [CMake] Include FLTK as suggested in the official docs
+- Add more unit tests for some Channel components
+- Minor cleanups and refactoring
+
+
+0.19.2 --- 2021 . 12 . 16
+- Fix wrong computation of soloed channels
+
+
+0.19.1 --- 2021 . 12 . 15
+- Enable JUCE_DEBUG in Debug builds
+- New MidiLighter tests + compile-time dependency injection
+- Set limits to minimum zoom level in Action Editors (#425)
+- Refactoring and code cleanup for Channel class and other sub-components 
+- Update JUCE to version 6.1.2
+- Update RtAudio to version 5.2.0
+- Sanitize MIDI ports values (fixes #515)
+- MidiLighter improvements and cleanups (fixes #517)
+- Fix off-the-beat metronome (#522)
+- Fix number of plug-ins found not being updated after a scan (fix #523)
+- Fix PluginManager initialization
+- Fix pthread linking in CMake (#520)
+- Fix build info not being printed correctly on startup
+- [Linux] Fix X error messages on closing some plug-in editors
+- [Linux] Fix wrong icon file in XDG desktop file
+
+
+0.19.0 --- 2021 . 11 . 01
+- New "One-shot Pause" channel mode
+- Refactoring: new component-based architecture
+- Fix crash on startup if recording from mono input
+- Improved event handling for plug-ins GUIs 
+- Fix many compiler warnings on menu items initialization
+
+
+0.18.2 --- 2021 . 09 . 13
+- New stereo In/Out audio meters 
+- Revamped Action Editor: better UI, improved usability 
+- Show play head in Action Editor
+- Implement queue for MIDI events, fix issue #482
+- Simplified Event Dispatcher's Event type
+- Move JACK transport operations to new JackTransport class
+- Always pick sample rate from the first audio device when using JACK
+- Don't send MIDI events if MIDI channel is not playing (#499) or muted (#497)
+- Add AtomicSwapper as git submodule
+- Upgrade JUCE to version 6.1.0
+
+
+0.18.1 --- 2021 . 07 . 25
+- New resampler architecture: allows for changing quality also for live rendering (#288)
+- Gracefully shutdown UI on close to random crashes on quit on Windows 
+- Fix 'one shot channels with actions as loops' mode not working correctly
+- Fix wrong sequencer signals while starting/stopping action recs with JACK (#397)
+- Fix extra dot in unique audio file name generation
+- Fix sample overflow when looping a sample with pitch != 1.0
+- [CMake, Linux] Detect JACK with pkg-config
+- [CMake, Linux] Install Freedesktop files and icon
+- [CMake, Linux] Add configure switches for ALSA, JACK and PulseAudio
+- [macOS] Enable hardened runtime
+
+
+0.18.0 --- 2021 . 05 . 18
+- New 'free loop-length' audio recording mode (#63)
+- Many AudioBuffer improvements
+- Audio configuration panel refactoring
+- KernelAudio improvements and cleanups
+- Relaxed BPM handling when working with JACK
+- Install executable to FHS compliant location (#450)
+- [CI] Don't UPX binaries on macOS (#459)
+- Fix Overdub protection ON by default not working (#460)
+- Fix crash when moving up from a deleted folder (#455)
+
+
+0.17.2 --- 2021 . 03 . 29
+- New double-buffered audio engine
+- Improved audio sample rendering precision
+- Show tooltips when hovering over UI components
+- Add .clang-format file 
+- Removed support for Autotools build system
+- Removed support for old raw patches
+- [CMake] Use find_package command for libsamplerate
+- Improved AudioBuffer move semantics
+- Send time + position information to plug-ins
+- Update JUCE library to version 6.0.7
+- Fix crash when saving project with plug-ins in invalid state
+
+
+0.17.1 --- 2021 . 02 . 01
+- Better CMake dependency management
+- Add CMake install rules (#422)
+- Switch to GitHub Actions for CI and release builds (#440)
+- Remove hardcoded 'test' folder in test suite (#432)
+- Make sure macOS minimum target is set to 10.14 (#444)
+- Fix crash when restarting after setting jack as an audio server (#409, #368)
+- Fix crash when clicking "Cancel" button in Browser dialog (#430)
+- Fix wrong action ID mapping when cloning a channel (#426)
+- Fix scrambled MIDI bindings (#427)
+
+
+0.17.0 --- 2020 . 11 . 15
+- Add CMake build system
+- VST3 support
+- Show descriptive plug-in names in Plug-in List Window
+- Resizable plug-in list
+- New persistence mechanism for Plug-ins state
+- Improved text truncation for small buttons and text boxes
+- Beautify Sample Editor window
+- Resizable plug-in list window
+- Show descriptive plug-in name in plug-in list
+- Update JUCE, version 6.0.4
+- Update Catch2 to version 2.13.2
+- Replace old filesystem functions in fs.h with std::filesystem
+- Add VST3 SDK as git submodule
+- Set minimum macOS version to 10.14
+- Statically link the MSVC runtime library on Windows
+- Avoid crash on opening plug-in list with invalid plug-ins
+- Rewind sample channels in loop.once.bar mode on bar, if still playing (fix #403)
+- Modernize log::print() function to handle std::string arguments (PR #402)
+- Fix playStatus logic for ending sample channels in loop-once-bar mode (#404)
+- Fix shrinking beats that could glitch the output (#361)
+
+
+0.16.4 --- 2020 . 09. 19
+- Support for mono inputs
+- Overdub mode for Sample Channels with optional overdub protection
+- Disable record-on-signal mode when sequencer is running
+- Shift + [click on R button] kills action reading when "Treat one-shot channels
+  with actions as loops" option is on
+- Start MIDI channels automatically after action recording session
+- Fix wrong sample rate conversion when project rate != system rate 
+- Fix Wrong begin/end sample markers when loading a project with 
+  samplerate != system.samplerate 
+- Fix wrong MIDI learn mapping for master parameters
+- Fix BPM button disabled after audio recording session
+
+
+0.16.3 --- 2020 . 06. 15
+- Non-virtual Channels architecture
+- Added G_DEBUG macro
+- Optimized CPU usage when playing with many channels
+- Increased UI refresh rate to 30 frames per second
+- Improved quantizer precision
+- Simplified behavior when halting channels containing recorded actions 
+- Fix wrong audio sample looping with pitch != 1.0
+- Fix MIDI input master values not stored on quit
+- Fix One-shot press channel mode not working via mouse 
+- Fix Action recording overlap (both live and via Action Editor)
+- Fix crash when loading a project with missing audio files
+- Fix BPM not changing via Jack
+
+
+0.16.2 --- 2020 . 02 . 18
+- Switch to Json for modern C++ library for reading and writing Json data
+- Resizable channels, improved version
+- Drop support for raw patches (still readable for backward compatibility)
+- Simplify global configuration parameters
+- Simplify column data storage in patch files
+- Center all micro-subwindows to screen
+- Revamped MIDI learning algorithm and related UI components
+- Always display 'R' button in Sample Channel
+- Don't download external files for unit tests
+- Optimized UI drawings for base buttons
+- Move build info from 'About' window to console log
+- Update RtAudio to 5.1.0
+- Fix crash during audio recording after opening a project (thanks AdTb!)
+
+
+0.16.1 --- 2020 . 01 . 08
+- FreeBSD support
+- Ability to remove empty columns manually
+- Gray out bpm value when in JACK client mode
+- 'Reset to init state' becomes 'close project' under File menu
+- [Linux] Upgrade Travis CI Linux machine to Xenial
+- Add namespaces to file system and logging functions 
+- Remove unused G_quit global variable
+- Fix Sample Channels in loop mode not playing automatically after audio 
+  recording
+- Fix action recording button status during audio recording, signal mode
+
+
+0.16.0 --- 2019 . 12 . 02
+- Fix columns' resizer bar height on vertical window resize
+- Fix crash on MIDI learn global commands
+- Fix wrong channel routing when triggering MIDI learnt commands
+- Fix rewind button not rewinding sample channels in LOOP_* mode 
+- Use actual buffer size from KernelAudio when loading channels from a patch
+- Remove FLTK multithreading initialization
+
+
+0.16.0 beta-2 --- 2019 . 11 . 11
+- Remove all pthread.h leftovers 
+- Fix Windows build
+- Fix memory corruption on Keyboard refresh
+- Fix wave size corruption while editing samples in Sample Editor
+- Fix freeze when cloning a Sample Channel with a sample in it
+- Fix buffer overflow when playing an edited sample
+- Fix crash when loading a project with missing plug-ins
+- Fix freeze when pressing 'play' during an audio recording session
+- Fix play/ending UI status of MIDI channels
+- Fix plug-in sorting on reload
+- Fix crash when reloading a sample in the Sample Editor
+- Fix messy 'R' button status when toggled
+- Fix missing icons and broken checkboxes
+- Optimize model updates on keyboard interaction
+- Always read Columns data from patch files
+- Show missing (and removable) plug-ins in Plug-in Window list
+- Create default empty columns on 'Reset to initial state'
+- Save relative Wave paths in project files
+
+
+0.16.0 beta-1 --- 2019 . 10 . 19
+- Fix macOS build error + warnings
+
+
+0.16.0 beta-0 --- 2019 . 10 . 19
+- New internal engine<->UI architecture 
+- New persistence layer
+- New MIDI queue for incoming live MIDI messages
+- Switch to std::thread
+- Absolute #include paths in source code
+- Removed Boost parameter from Sample Channel 
+
+
+0.15.4 --- 2019 . 03 . 22
+- New record-on-signal option for input and action recording
+- Initial support for plug-ins with mono I/O buses
+- PluginHost refactoring
+- Smart pointers for Wave and Plugin objects
+- Remove old and deprecated input delay compensation
+- Optimized audio IO processing in Mixer callback
+- Atomic I/O meters with improved accuracy
+- Fix memory leak when replacing samples in a Sample Channel
+- Fix plug-ins ordering method when re-opening Giada
+- Fix silent Sample Channel when recording actions a second time
+- Fix velocity always discarded when sending key-press to Sample Channel
+- Fix inability to record actions with quantizer enabled
+
+
+0.15.3 --- 2018 . 12 . 24
+- Action recorder refactoring
+- Optional midimap parameters (thank you @tomek-szczesny)
+- Support for "inaudible" MIDI lightning events (thank you @tomek-szczesny)
+- Build AppImage for Linux on Travis CI instance
+- Huge optimization of the AppImage binary file
+- Fix Action Editor repaint on min/max zoom levels
+- "Resize recording" flag has been removed
+- Change text labels for channel operations
+- Smarter column assignment while loading a patch/project
+- Fix wrong resizer bar width between Action Editor widgets when zooming
+- Can't display custom channel name in Sample Channel (fixed)
+- Fix crash when cloning Sample Channel with audio data in it
+- Clone channel doesn't clone channel name (fix #219)
+
+
+0.15.2 --- 2018 . 09 . 05
+- New sample-accurate Action Editor
+- New MIDI Velocity Editor widget
+- Ability to move MIDI events vertically in piano roll (i.e. change note) 
+- Remove mute action recording
+- Better handling of MIDI devices that send NOTEON + velocity 0 as NOTEOFF
+- Avoid calls to deprecated JUCE plug-ins methods
+- Removed useless pthreadGC2.dll from Windows package
+- Can't kill MIDI channels (fix #197)
+- Can't record MIDI actions (fix #202)
+- Fix missing first beat on metronome rendering
+- Fix crash on opening plug-in window on macOS
+
+
+0.15.1 --- 2018 . 07 . 03
+- Deep code refactoring, featuring Channels processors
+- Many new unit tests added
+- Simplify mutex mechanism
+- Fix wrong quantizer value on patch/project load
+- Remove the old, buggy and glitchy internal crossfade algorithm
+- Fix many potential plug-in crashes on Linux
+- Properly close plug-in window on plug-in removal 
+- Improve BPM changes while running as JACK client
+
+
+0.15.0 --- 2018 . 04 . 18
+- Refactor audio engine into frame-based processing
+- Refactor channels readers/writers into channelManager namespace
+- Smarter Solo algorithm
+- Fix missing .wav extension on recorded audio takes
+- Fix wrong Channel status update after 'Clear all actions'
+
+
+0.14.6 --- 2018 . 03 . 15
+- MIDI velocity drives volume for one-shot sample channels
+- FLAC and Ogg support
+- Ability to use system-provided Catch library (GitHub #151)
+- Update Catch to version 2
+- Fix unreadable tabs title in Configuration Window (GitHub #168)
+- Fix crash on opening About window
+- Fix 'read actions' button behavior during ending and waiting statuses
+- Fix sound card initialization on MacOS
+- [Windows] Fix UI stuck on top-right corner
+- [Windows] Fix browsing for directories
+
+
+0.14.5 --- 2018 . 01 . 15
+- OS X builds on Travis CI
+- AppImage executable for Linux
+- Support for multiple plug-in directories
+- New directory browser for adding plug-in directories
+- Update plug-in's parameters on program change in plug-in's window
+- Improved MIDI action management in Piano Roll
+- Simplified conditional rules in Makefile.am 
+- Fix crash on MIDI learn for plug-in parameters
+- Fix crash in MIDI input window if MIDI in params are 0
+- Fix unwanted new action when dragging piano items in Piano Roll
+- Fix crash while recording on existing project (GitHub #161) 
+- Fix crash on startup in Windows build
+
+
+0.14.4 --- 2017 . 10 . 28
+- Renameable channels
+- Portable VST path
+- [Sample Editor] Sample shift tool
+- [Linux/Mac] Don't skip '/' path when navigating to upper folders
+- Ability to process more than one plug-in instrument at once
+- Beautify Configuration Window
+- Bring VST window to front when opening UI
+- Save 'arm' status to patch/project file
+- Revamped Beats and Bpm input windows
+- Simplified audio samples' storage in project folders
+- Update JUCE to version 5.1.2
+- UI-less plug-in window refinements
+- Update UI-less plug-in window on MIDI parameter's change
+- Strip .gptc/.gprj extension from patch name
+- [Sample Editor] Fix non-working 'cut' operation
+- Fix missed MIDI events with more than 1 plug-in in the stack
+- Fix File Browser path widget drawn incorrectly in OS X
+- Fix missing MIDI learn for 'Arm channel' and 'Kill channel'
+
+
+0.14.3 --- 2017 . 09 . 18
+- [Sample Editor] New "reverse selection" function
+- [Sample Editor] New "normalize hard" function
+- [Sample Editor] New "copy to channel" function
+- [Sample Editor] New "copy & paste" function
+- [Sample Editor] Double click on waveform selects all
+- [Sample Editor] Fix garbled characters in window's title
+- [Sample Editor] Fix wrong result on "set pitch to song/bar"
+- Resizable channels
+- Remove calls to malloc/free in Mixer (use new/delete instead)
+- Improved UI management of VST plugins
+- Fix infinite loop for one shot retrig samples with quantizer > 0
+- Fix wrong geChannel count while saving a patch
+- Fix missing greyed-out options in Sample Channel's menu when loading a wrong
+  sample
+- Fix crash while audio recording with BPM set below the default 120 
+- Print correct octave numbers in Piano Roll
+
+
+0.14.2 --- 2017 . 08 . 14
+- [Sample Editor] Audible preview (with optional loop mode)
+- [Sample Editor] Frame-precise editing
+- [Sample Editor] Show sample's information
+- [Sample Editor] Improved fade out algorithm
+- [Sample Editor] Process both left and right channel's data while drawing
+- Better Wave objects handling
+- Improved channels' memory management
+- Improved empty columns cleanup algorithm
+- Update Catch version
+- Update JUCE version (5.1.1)
+- Update Jansson version (2.10)
+- Fix missing tempo update on reset to init state
+- Fix wrong memory allocation for UI-less plugins
+
+
+0.14.1 --- 2017 . 07 . 16
+- Update JUCE library to 5.0.2
+- Show play head in Sample Editor
+- Refactor pop up menu in Sample Editor
+- Many small fixes and optimizations in waveform drawing routine
+- Makefile cleanup
+- Fix crash while recording with beats/bars greater than 4/1 (GitHub #134)
+
+
+0.14.0 --- 2017 . 05 . 29
+- Sample Editor reorganized and refactored 
+- Removed support for old ini-based patch files
+- Improved and simplified pan algorithm
+- Ability to toggle input monitoring while recording audio
+- Lots of code refactoring
+- Convert all .h headers to C++ headers
+- Update Libsndfile to version 1.0.28
+- Fix crash when recording audio
+- Fix wrong file path when exporting samples
+- Fix a bug that prevented begin/end handles to work in Sample Editor
+- Fix Sample Editor's grid value not being stored properly on close
+
+
+0.13.4 --- 2017 . 04 . 23
+- Removed support for old ini-based MIDImap files
+- Initial support for channel-based MIDI filtering
+- New Orphaned MIDI events in Piano Roll editor
+- Improve action filtering in Piano Roll editor
+- Lots of code refactoring
+- New test suite for Action Recorder
+- Fix obscure bug when overdubbing actions and a null loop occurs
+- Fix "clear all actions" menu refresh when removing items on Piano Roll
+
+
+0.13.3 --- 2017 . 03 . 25
+- Strip VST folder from Git repository
+- Fix 'Close' button's position inside MIDI input window
+- Update RtMidi to version 2.1.1
+- Improve 'free channel' function (GitHub #105)
+- New 'Clock' structure for timing operations
+- New Jack implementation with BPM sync and Rewind (GitHub #89)
+- Fix missing tracker reset on 'free channel' function (GitHub #99)
+
+
+0.13.2 --- 2017 . 01 . 14
+- MIDI learn for plugins parameters
+- Toggle hidden files in File Browser
+- Fix broken compilation when build without VST support
+- Make sure PluginChooser window has a sane size
+- Decouple Recorder from any global variable
+- Better source code organization
+- Make plugin creation more robust
+- More source code reorganization
+- Fix crash on clicking scrollbar arrows (GitHub #53)
+- Fix crash when doubling/dividing length while recording (GitHub #110)
+
+
+0.13.1 --- 2016 . 11 . 16
+- Input MIDI to MIDI channels/plugins
+- Refinements to show/hide 'R' button's dynamics
+- Increase piano roll items' height
+- Set input volume to max by default
+- Start live-recorded sample channels right away
+- Avoid potential crashes when loading samples on running channels
+- Generate metronome during output post-processing
+- Better widgets' layout in Sample Editor
+- Lots of source code optimizations and cleanups
+- Fix inverted 'R' button's status (GitHub #94)
+- Better handling of 'R' button's status when the sequencer is off (GitHub #95)
+- Fix non-playing samples if live-recorded and 'R' button is on (GitHub #93)
+- Reset button statuses once channels have been freed (GitHub #100)
+- Fix missing ASIO and WASAPI APIs on Windows (GitHub #96)
+- Missing RtMidi libs on Linux (GitHub #102)
+- Fix fade-in/fade-out editing not triggering alert on save (GitHub #101)
+
+
+0.13.0 --- 2016 . 09 . 20
+- Deep file browser refactoring
+- Save browser's scroll position and last item selected on opening
+- Load patches/projects/samples on double click
+- 64 bit builds for Windows
+- Prevent deprecated patch from crashing if a plugin is not found in the stack
+- Force logger to flush to file on Windows
+- Add more default values for windows' dimensions and positions
+- Avoid crashes on Configuration panel if no midimaps were selected
+- Fix missing keyRelease actions in action editor
+- Update JUCE to version 4.2.3
+- Don't include JUCE on tests without VST support (GitHub #75)
+- Fix compilation errors on GCC 6 (GitHub #82)
+- Fix includes on OSX (GitHub #92)
+- Fix wrong channel's actions count that prevented "R" button to be toggled
+  properly
+- Fixed a bug that prevented actions on frame 0 to being properly reproduced
+- Make Recorder a proper class
+- Better naming convention for ActionEditor's children classes
+- Source code reorganization
+
+
+0.12.2 --- 2016 . 06 . 02
+- Update RtAudio to version 4.1.2
+- Add WASAPI support on Windows
+- Sortable plugins list
+- Simplify custom RtAudio build and inclusion on Linux
+- Fix crashes on startup on OS X El Capitan
+- Store position and size of Available Plugins window
+- Untangle Channels' code from global variables
+
+
+0.12.1 --- 2016 . 05 . 06
+- Show percentage progress for plugin scan
+- Notify if plugins are missing
+- Notify if unknown plugins are present
+- Fix potential segfault on MasterIn/MasterOut plugins loading
+- Proper cleanup of JUCE resources
+- Internal refactoring on PluginHost's global variables
+
+
+0.12.0 --- 2016 . 03 . 07
+- Port to JUCE Framework for audio plugin management
+- Increase global font size
+- Minor UI fixes and cleanups
+- Add ability to run tests outside Travis CI
+- Switch to C++11
+- 64 bit binaries for OS X
+- Use new constant for global font size
+
+
+0.11.2 --- 2016 . 01 . 16
+- New JSON-based midimap files
+- Add new channel by right-clicking anywhere on a column
+- Show warning if patch is using the deprecated file format
+- Do not force 32 bit compilation on OS X
+- Fix warnings and errors on GCC 5.3
+- Fix a bug that prevented MIDI Jack from being selected on Linux
+
+
+0.11.1 --- 2015 . 12 . 22
+- Ability to clone channels
+- New JSON-based configuration file
+- Port all vectors from old gVector to std::vector
+- Deactivate all other MIDI fields when changing MIDI system in Config window
+- Minor optimizations in configuration panel, Audio tab
+- Assume 'none' as default sound system
+- Include Catch header file in source package
+- Update Travis CI environment to Ubuntu Trusty
+- Fix missing sanitization after reading configuration file
+- Fix garbage text in device info window
+- Fix wrong config value if no midimaps are available
+- Fix garbage text while printing device and port names
+
+
+0.11.0 --- 2015 . 12 . 02
+- New JSON-based patch system
+- Properly store column width in patch
+- Port all const char* strings to std::string in patch/project glue layer
+- Switch to SemVer-like internal versioning system
+- More source code reorganization
+- Fix potential memory leaks in Mixer
+- Fix missing static link of RtMidi on Linux
+- Unable to store pitch values > 2.0 (fixed)
+- Missing assigned key after opening patch (fixed)
+
+
+0.10.2 --- 2015 . 10 . 21
+- Setup Travis CI automated builds
+- Add base framework for unit testing (with Catch)
+- Improve behavior of Loop Once family when the sequencer is halted
+- Fix empty sample path in sample channels when saving a Project
+- Fix disabled "edit actions" for sample channels
+- Fix missing pthreadGC2.dll in Windows build
+
+
+0.10.1 --- 2015 . 08 . 26
+- Massive source folders refactoring
+- Improved usability of "play" buttons for channels
+- Remove support for patches created with Giada < 0.6.x
+- Fix check for configured soundsystem (would break compilation on g++5)
+- Small fixes and cleanup in Makefile.am
+
+
+0.10.0 --- 2015 . 07 . 05
+- MIDI lightning output
+- Other minor fixes
+
+
+0.9.6 --- 2015 . 05 . 11
+- Keyboard binding for MIDI channels
+- Support for multiple files in drag-n-drop operations
+- Different color for wait/end statuses
+- Small improvements to Keyboard grabber widget
+- Fix random crashes with Jack enabled
+- Fix weird behavior with multiple drag and drop
+- Code refactoring
+
+
+0.9.5 --- 2015 . 03 . 28
+- Better column resize algorithm
+- New patch loading system with permanent MIDI mapping
+- Ability to clear assigned keys (keyboard mode)
+- Improved zoom icons in editors
+- Fix deprecation warning in configure.ac
+
+
+0.9.4 --- 2015 . 02 . 24
+- Drag-n-drop now works also in existing channels
+- Store 'resize recordings' flag in giada.conf
+- Better management of duplicate samples
+- Add more VST debug information
+- Minor fixes and tweaks
+
+
+0.9.3 --- 2015 . 02 . 01
+- New GUI improvement: responsive and resizable columns
+- Upgrade to FLTK 1.3.3
+- More robust column handling mechanism
+- Support for MIDI devices without note-off message (@blablack)
+- Fix segfaults when saving a patch with missing plugins
+- Fix many minor graphical bugs
+- Fix wrong vector assignment in MIDI send event
+- Fix reloaded patches with no right tempo/beats displayed
+- Fix random odd frames when adding/moving events in Piano Roll
+- Minor internal cleanup
+
+
+0.9.2 --- 2014 . 11 . 29
+- New grid layout in Sample Editor
+- Load samples via drag n drop
+- Add new utility functions: gTrim and gStripFileUrl
+- Fix "normalize" button position in Sample Editor
+- Minor waveform drawing optimizations
+- Add missing files for RtAudio-mod compilation
+- All one-shot mode, if fired manually, get the first frame truncated (fixed)
+
+
+0.9.1 --- 2014 . 09 . 24
+- Bring back custom version of rtAudio in source package
+- Automatically turn up volume when adding new channel
+- Updated 'misc' tab in configuration panel
+- Fix startup crash on OS X
+- Fix missing jack headers
+
+
+0.9.0 --- 2014 . 08 . 18
+- New full-screen GUI
+- Multi-column support
+- Advanced logging system
+- Upgrade to RtAudio 4.1.1 and RtMidi 2.1.0
+- Removed embedded RtAudio (thanks to Arty)
+- Fix wrong processing of VST MIDI events on 64 bit version
+- Fix stretched buttons when resizing sample editor window
+- "Clear all samples" destroys channels (fixed)
+- "Free channel" messes up loop / mute buttons (fixes)
+- Fix potential recordings with odd frames
+
+
+0.8.4 --- 2014 . 03 . 27
+- New mode 'Loop Bar Once'
+- Several small improvements and cleanups to internal utils functions
+- Fixed missing title in several subwindows
+- (win) Fix runtime error when loading a new project
+- Fix chan reset when clicking on waveform
+- Properly close subwindows after a channel has been deleted
+- Fix 'reload' button not working for samples with updated names
+
+
+0.8.3 --- 2014 . 02 . 14
+- Experimental MIDI timing output with MTC and MIDI clock
+- Expose Sequencer x2 and /2 via MIDI
+- New pitch operators x2 and /2
+- Internal xfade process restored
+- "set key..." becomes "setup keyboard input" for sample channels
+- MIDI events are now saved as unsigned int in patch
+- Same expression on both sides of '|' in recorder.cpp (fixed)
+- Muted channels leak some glitches on 'kill' event (fixed)
+- Piano roll can't be edited anymore if beats == 32 (fixed)
+- Noise when adding new MIDI channel (fixed)
+- Boost and Normalize not working (fixed)
+- Multiple copies of every file used by the patch (fixed)
+- Samples with -1, -2, ... -n suffix are not included in patch (fixed)
+- Segfaults when quantizing samples (fixed)
+
+
+0.8.2 --- 2014 . 01 . 13
+- Pitch control exposed via MIDI
+- New tools in Sample Editor (linear fade in/out, smooth edges)
+- Implemented vstEvent->deltaFrames, gaining more precision with vst
+       MIDI events
+- Add Fl::lock/Fl::unlock dynamics to glue_ calls where needed
+- Avoid pitch sliding when changing pitch of a sample in status OFF
+- Update copyright info in source files
+- Internal fade in and fade out restored
+- Add 'Giada' keyword to desktop file
+- Fix annoying glitches when playing very short samples
+- Fix random crashes when controlling giada via MIDI
+- Fix missing MIDI mapping for read-actions button
+
+
+0.8.1 --- 2013 . 12 . 09
+- New, high-quality pitch control based on libsamplerate
+- New set of functions 'spread sample to beat/song'
+[known issues]
+- Internal crossfades have been temporarily disabled. Some clicks may
+       occur
+
+
+0.8.0 --- 2013 . 11 . 03
+- Initial MIDI input support
+- Fix freeze when recording audio inputs on a second channel
+- Fix 'R' button to show up even if the channel has no actions
+- Fix weird drawings of keypress actions in action editor
+- Free channel: delete 'R' button as well
+- Shift+key does not kill loop mode channels in a wait status
+- Fix issue with 'R' button and newly added actions
+- Remove "left"/"right" labels from main buttons
+
+
+0.7.3 --- 2013 . 09 . 14
+- Experimental 64 bit compilation (Linux only)
+- Massive internal cleanup of channel/gui channel layers
+- Set default mode to full volume on sample load
+- Set default mode to oneshot basic
+- Faster drawings in piano roll
+- Visual aids in piano roll
+- Scroll to pointer in piano roll
+- Several minor improvements in piano roll's usability
+- Revised VST Carbon window popup system
+- Minor improvements in startInputRec/stopInputRec procedure
+- Fix compile error using local type Plugin* in Channel's constructor
+- Fix segfault in OSX when working with VST windows
+
+
+0.7.2 --- 2013 . 07 . 27
+- Initial MIDI output support
+- Mute now affects channels with VSTi signals
+- Lots of deb package improvements
+- Complete rewrite of VST GUI part on OS X
+- Don't send MIDI mute on sample channels
+- Send MIDI mute for MIDI channels in play mode
+- Fix wrong looping due to VST processing in mixer::masterPlay
+- Fix jack crashes when using Giada with ALSA
+- Fix VST random crashes on OSX, bus error
+- Fix input device set to -1 after a system change
+
+
+0.7.1 --- 2013 . 06 . 27
+- Initial Jack Transport support
+- Send global note off when sequencer is being stopped
+- Send note off when deleting notes in Piano Roll
+- Store position and size of Piano Roll in conf file
+- Avoid overlap MIDI notes in Piano Roll
+- MIDI channel refactoring
+- MIDI channels now behave like loop-mode ones
+- Fix graphical bugs in Action Editor, sample mode
+- Fix refresh issue in Piano Roll when deleting items
+- Lots of invisible cleanups and improvements
+
+
+0.7.0 --- 2013 . 06 . 05
+- Initial MIDI output implementation
+- Initial VSTi (instrument) support
+- New piano roll widget in action editor
+- New chan mode: MIDI vs SAMPLE
+- Fix E-MU Tracker Pre not correctly listed in audio in/output
+
+
+0.6.4 --- 2013 . 05 . 07
+- Resizable plugin parameter window
+- New and standard package name format <name>-<version>.<ext>
+- Implement RtAudio::getCompiledApi() to fetch compiled APIs
+- Implement audioMasterGetSampleRate, audioMasterGetLanguage VST opcodes
+- Add drop-down menu for buffer size values in config panel
+- Enhance project portability between OSes
+- Lots of fixes and improvements for VST strings and parameters
+- Avoid segfault when loading recs from a patch with files not found
+- Always remember selected program when shifting up/down plugins
+- Fix wrong size of single_press displayed in action editor
+- Fix volume actions resized with value set to zero
+- Fix volume envelope always over the cover area
+- Fix src package extracts to current dir
+- Fix segfault in loadpatch process if plugin GUIs are open
+- Fix segfault when closing patch with plugins in BAD status
+
+
+0.6.3 --- 2013 . 04 . 23
+- New 'solo' button
+- Portable project system
+- New 'Single Endless' channel mode
+- GUI enhancements for channels in WAIT or ENDING status
+- Minor fixes & cleanups
+
+
+0.6.2 --- 2013 . 04 . 05
+- New volume envelope widget
+- Zoom with mouse wheel in the action editor
+- Graphical enhancements & speedups for the action editor
+- Loop-repeat doesn't stop when put in ending mode (fixed)
+- Fix draw errors when zooming too much the action editor
+- Set silence in wave editor messes up the waveform (fixed)
+- Wrong slashes in file path when saving a patch in Windows (fixed)
+- Many, many code improvements and bugs fixed
+
+
+0.6.1 --- 2013 . 03 . 21
+- Unlimited number of channels
+- Deep internal refactoring, mixer/GUI layers
+- Fix random crashes on exit
+- Fix crashes when closing Giada with VST windows opened
+- Always free Master In plugin stack on exit
+- Lots of other minor bugs fixed and small enhancements
+
+
+0.6.0 --- 2013 . 03 . 02
+- New, full-screen, redesigned sample editor
+- Zoom with mouse wheel in sample editor
+- Use kernelAudio::defaultIn/defaultOut for DEFAULT_SOUNDDEV_OUT
+- Volume knob in main window now updates the editor
+- Sound system issues in OS X (fixed)
+- Output device info dialog refers to wrong device (fixed)
+
+
+0.5.8 --- 2013 . 02 . 07
+- Internal samplerate conversion (with libsamplerate)
+- Bring channels automatically to full volume on sample load
+- Ability to set the audio device frequency
+- New "internal mute" feature
+- fix for deprecated VST opcode 14
+- fix deb package issues on Ubuntu 12.10 / KXStudio
+
+
+0.5.7 --- 2013 . 01 . 21
+- visual grid + snapping in the action editor
+- implement more audioMasterCanDo's in pluginHost
+- limit zoom in actionEditor
+- revise zoom behavior in actionEditor, now more comfortable
+- fix forward declaration & inclusion of several headers
+- implemented VST opcode 32
+- implemented VST opcode 33
+- implemented VST opcode 34
+- update website link in tar files
+- update copyright info for 2013
+
+
+0.5.6 --- 2013 . 01 . 03
+- New overdub mode for live recording
+- Support for VST programs, aka presets
+- Lots of VST opcodes implemented
+- Fix crash when removing a plugin from the stack
+- Fix pops when going to beat 0
+- Fix compilation issues without --enable-vst
+- Many invisible optimizations and small bugs fixed
+
+
+0.5.5 --- 2012 . 12 . 15
+- "Hear what you're playing" feature
+- Fx processing on the input side
+- Ability to add different action types (Action Editor)
+- Desktop integration on Linux (via deb package)
+- Upgrade to FLTK 1.3.2
+- Remove "the action might stop the channel" when loading new samples
+- Fix wrong positioning of zoom tools (Action Editor)
+- Fix unwanted interactions on the grey area (Action Editor)
+- Fix wrong memory alloc during the VST processing
+- VST don't show up in OS X (fixed)
+- Minor internal refactoring + bugfixing
+
+
+0.5.4 --- 2012 . 11 . 24
+- VST GUI support
+- Better subwindow management
+- Implemented many other VST opcodes
+- Missing plugins are now shown in the list with a 'dead' state
+- Refresh action editor when changing beats (via beat operator or
+  beat window)
+- Graphical improvements in the action editor
+- Resizable action editor doesn't work well (fixed)
+- Fix auto fadeout for SINGLE_PRESS channels
+- Fix compilation without --enable-vst
+- Fix for a wrong prototype definition of the VST hostCallback
+
+
+0.5.3 --- 2012 . 10 . 26
+- Live beat manipulators (x2)(/2)
+- New sub-windows management, faster and more comfortable
+- New optional hard limiter on the output side
+- Action Editor window recalls x,y,w,h zoom and position
+- Usability improvements while handling an action (action editor)
+- Refresh actionEditor window when switching channel mode or delete
+       actions
+- Unable to delete a killchan action (action editor) (fixed)
+- Don't show ACTION_KILLCHAN in a singlepress channel (action editor)
+- Libsndfile no longer statically linked in Linux
+- Fixed a typo in config: "when the sequeCer is halted"
+- redefinition of DEFAULT_PITCH in wingdi.h (windows) (fixed)
+- Upgrade to FLTK 1.3.0
+- Other internal optimizations
+- Other small bugs fixed
+
+
+0.5.2 --- 2012 . 10 . 05
+- Add ability to handle actions for loop-mode channels
+- Add ability to record live mute actions for loop-mode channels
+- Lots of live action recording improvements
+- Enhanced usability for the action editor
+- More verbose output if kernel audio fails to start
+- Several internal optimizations
+
+
+0.5.1 --- 2012 . 09 . 13
+- First implementation of the Action Editor
+- Added compatibility with Ubuntu >= 10.04
+
+
+0.5.0 --- 2012 . 07 . 23
+- New custom project folder (.gprj)
+- Sample names are now made unique
+- Fixed unwanted time stretching while exporting a mono sample
+- Lots of minor internal improvements
+
+
+0.4.12 --- 2012 . 07 . 01
+- VST parameters and stacks are now stored in patch file
+- Upgrade to RtAudio 0.4.11
+- PulseAudio support in Linux (thanks to RtAudio 0.4.11)
+- Revised .deb package
+- Enhanced "normalize" function in wave editor
+- Several memory issues fixed
+- Internal enhancements and minor bugs fixed
+
+
+0.4.11 --- 2012 . 06 . 10
+- VST stack for each channel
+- Custom paths for plugins, samples and patches
+- Crash in config panel if device is busy (fixed)
+- Graphical bug in the input meter (fixed)
+- ParamLabel added in the VST parameter list
+
+
+0.4.10 --- 2012 . 05 . 30
+- Ability to shift up an down VST plugins
+- Enhanced patch/conf architecture
+- Ability to edit a sample while playing
+- Mutex controls in VST processing
+- Lots of security issues fixed while changing pitch dynamically
+- Enhanced sub-window system
+- Several minor bugs fixed
+
+
+0.4.9 --- 2012 . 05 . 12
+- No more mandatory inputs
+- Pitch value properly stored inside the patch
+- Several small VST host improvements
+- Enhanced window management
+- Ability to browse files while playing with main GUI (non-modal browser)
+- Improved error checking in KernelAudio
+- Wrong style for lower scrollbar in Browser (fixed)
+- Fixed compilation on 64 bit systems (thanks to Speps@Archlinux)
+- Samplerate no longer hardcoded, auto-detected with JACK
+- Minor internal improvements and bugfixing
+
+
+0.4.8 --- 2012 . 04 . 21
+- Initial VST support (experimental)
+- Pitch controller (experimental, no filtering)
+- OSX bundles are now correctly handled by the file browser
+- Fixed several memory leaks
+- Minor internal improvements
+
+
+0.4.7 --- 2012 . 03 . 31
+- Cut, trim & silence operations in sample editor
+- New "Reload sample" button added
+- Lots of optimizations in the waveform drawing routines
+- The sample is no longer editable while in play mode
+- Fixed potential startup crashes while using Giada with Jack Audio
+- Other minor fixes applied to the configuration panel
+- Fixed compilation on 64 bit systems (thanks to Speps@Archlinux)
+
+
+0.4.6 --- 2012 . 03 . 11
+- New device information panel
+- The device configuration now shows only active and available devices
+- Channel panel no longer pops up during a recording process
+- GUI beautifications and other minor graphical fixes
+- Program icon added in all subwindows
+- Action records no longer available during a take, and vice versa
+- Fixed a serious bug that swapped input and output devices
+- Fixed loop behavior in ending mode
+- Fixed clicks when stopping a muted channel in loop
+
+
+0.4.5 --- 2012 . 02 . 25
+- Complete GUI redesign
+- New "start/stop action recs" button
+- Lots of internal cleanups and micro refactorings
+- Small drawing glithes in Editor and status box (fixed)
+- An invalid patch puts Giada to init state (fixed)
+- Fixed button repeat on start/stop, action rec, input rec
+- Checks against takes with unique name
+- Message "this action may stop the channel" always shown (fixed)
+- Channel no longer freeable while a take is in progress
+
+
+0.4.4 --- 2012 . 02 . 04
+- New input/output channel selector
+- Rewind bypasses the quantizer if triggered via mouse (fixed)
+- Fixed library paths in configure and makefile (thanks to Yann C.)
+- Added AUTHORS and NEWS files to the source package (thanks to Yann C.)
+- More robust sample export procedure
+- Issues with mute buttons when opening a patch (fixed)
+- Several usability improvements
+- Minor code cleanups and optimizations
+
+
+0.4.3 --- 2012 . 01 . 21
+- New "save project" feature
+- Ability to export a single sample to disk
+- More feedback when removing/clearing actions and samples
+- Sequencer starts automatically when action-rec button is pressed
+- Alert if patch name is empty while saving it
+- Channels now store internally the name of the samples
+- Missing "--no devices found--" in input devices menu (fixed)
+- Alert added if there are no empty channels for recording
+- "Edit->Clear all actions" no longer works (fixed)
+- END button could be used as a channel trigger (fixed)
+- Recorders are available even if device status is wrong (fixed)
+- Missing sample rewind if channel is muted (fixed)
+- Quantizer doesn't work if framesize is odd (fixed)
+- Random segfault when closing Giada (fixed)
+- Lots of code cleanups
+- Other minor improvements and optimizations
+
+
+0.4.2 --- 2012 . 01 . 09
+- Live sampling from external input with meter and delay compensation
+- Check against uneven values and overflow in buffersize field
+- Wrong normalized values if volume level is 0.0 (fixed)
+- Boost dial goes crazy if normalized > 20.0 dB (fixed)
+- Boost dial goes crazy if normalized < 0.0 dB (fixed)
+- Unwanted noise click if a muted channel is being rewinded (fixed)
+- Mute doesn't work well for single-shot samples (fixed)
+- Wrong FLTK headers (fixed, thanks to Yann C.)
+- Moving chanStart/chanEnd swaps stereo image (fixed)
+- Reset to init state doesn't reset mute buttons (fixed)
+- Wrong chanStart value if > 0 (fixed)
+
+
+0.4.1 --- 2011 . 12 . 07
+- Complete mixer engine refactoring
+- Faster audio buffer allocation
+- Global beat system revisited
+- Autocrossfade between samples is now enabled by default
+- No more recorded actions on odd frames
+- Unintentional channel swapping fixed
+- Unable to list all sound systems and sound devs under OSX (fixed)
+- Missing graceful stop of audio streaming under OSX (fixed)
+
+
+0.4.0 --- 2011 . 11 . 16
+- Support for all major uncompressed file formats (with libsndfile)
+- Enhanced mono > stereo conversion
+- Fixed drawing issues for the start/stop labels inside the waveform
+- Enhanced backward compatibility with old patches
+- Support for compilation on OS X and Windows
+
+
+0.3.6 --- 2011 . 11 . 02
+- Initial Mac OS X release
+- (Windows) Ability to list and browse all active drives
+- Change some internal routines plus minor optimizations
+- Added -pedantic and -Werror flag to the compiler
+- Crash if clicking on mute in an empty channel (fixed)
+- Chan status changes if an empty channel is being muted (fixed)
+
+
+0.3.5 --- 2011 . 10 . 22
+- Pan controller added
+- New GNU-style source code packaging
+- Revamped .deb package
+- Program icon missing under Windows (fixed)
+- Crash if a sample in patch is missing from the filesystem (fixed)
+- Unable to rewind to beat 1 if quantizer is on and seq stopped (fixed)
+- Several minor glitches fixed
+
+
+0.3.4 --- 2011 . 10 . 10
+- Full source code released under GPL license
+- Autosmooth is now toggleable via setup
+- Faster loading process of patch files
+- Various internal cleanups and optimizations
+- Fixed incorrect reading of boost values from patch
+- Fixed a potential bug that prevented the config panel to appear
+- Fixed stereo swap bug
+- Minor graphical revisions
+
+
+0.3.3 --- 2011 . 09 . 28
+- New "normalize" function
+- More editing tools added inside the sample editor
+- Waveform beautifications
+- Fixed interaction bugs for boost and volume controls
+
+
+0.3.2 --- 2011 . 09 . 19
+- New "mute" button inside the main window
+- Waveform is now updated when the boost value changes
+- Zoomin/zoomout relative to the scrollbar position
+- Fixed garbage output if the volume was "-inf" (windows version)
+- Fixed several rendering issues for short waveforms
+
+
+0.3.1 --- 2011 . 09 . 12
+- Boost volume + fine volume control in sample editor
+- Start/End handles inside the editor are now draggable via mouse
+- Fixed scrollbar issues in sample editor
+- Start/end points are now always drawn in the foreground
+- Waveform no longer overflow if a value is greater than the window
+- (linux) giada.conf is saved inside the hidden folder /home/.giada
+- patch loading process is now faster and cleaner
+- Update to rtAudio 4.0.10
+
+
+0.3.0 --- 2011 . 09 . 01
+- New sample editor window
+- Ability to set start/end points within a sample
+- Update to rtAudio 4.0.9
+- Fixed an string overflow inside a patch
+- Fixed a missing memory free if a sample is unreadable
+- Several internal updates and optimizations
+
+
+0.2.7 --- 2011 . 07.  22
+- New way to handle recorded channels as loops
+- Fixed retrig for backspace key (rewind)
+- Enhanced rewind with quantization support
+- Main and alert windows now appear centered on screen
+- Sanity check against old patches without metronome information
+- Rewind now affects loops in rec-reading mode
+
+
+0.2.6 --- 2011 . 07 . 11
+- Internal metronome
+- Fixed some glitches in config panel
+- Minor cleanups
+
+
+0.2.5 --- 2011 . 06 . 20
+- Configuration panel redesign
+- Several new control options
+- Progress feedback when loading patches
+- Internal optimizations
+- Updated docs
+
+
+0.2.4 --- 2011 . 06 . 08
+- New loop repeat mode
+- Ability to save patches anywhere in the filesystem
+- Sub-beat management
+- Sound meter has been revisited and improved
+- Several patch enhancements
+- Core audio optimizations
+
+
+0.2.3 --- 2011 . 05 . 18
+- ASIO support for Windows version
+- Enhanced security when reading values from a patch
+- Ability to disable the recordings when the sequencer is paused
+- Master volume and rec status are now saved inside the patch
+- Device selection fixed and improved
+- Sequencer flickering in Windows has been fixed
+- Feedback added if a sample from a patch is unreadable or corrupted
+- Minor internal optimizations
+
+
+0.2.2 --- 2011 . 05 . 04
+- New open-source patch system
+- A patch can now be loaded from any location of the filesystem
+- Enhanced file browser coords system
+- Lots of minor improvements to the sample loading/unloading procedure
+- (win) Init path of file browser now starts from %userProfile%/Desktop
+- Wrong handling of "/" chars fixed in config menu
+- Fixed potential hangs on quit
+- Fixed clicks when stopping sequencer/sample
+- Minor gui beautifications
+
+
+0.2.1 --- 2011 . 04 . 26
+- Windows version
+
+
+0.2.0 --- 2011 . 04 . 19
+- Full JACK and ALSA support with RtAudio
+- New list of sound devices in menu window
+- Enhanced shutdown procedure to prevent potential crashes
+- Some GUI glitches fixed
+- Fixed random locks when the screensaver is active
+
+
+0.1.8 --- 2011 . 04 . 13
+- new functions: free al samples/recordings, reset to init patch
+- main menu redesign
+- the file browser is now resizable
+- GUI feedback for samples in play mode
+- some fixes when unloading a sample
+
+
+0.1.7 --- 2011 . 04 . 07
+- Ability to remove only action recordings or mute recordings
+- Shift+key now stops the sample if the master play is deactivated
+- Frame 0 was always processed at the end of the sequencer
+- Minor internal improvements
+
+
+0.1.6 --- 2011 . 03 . 29
+- Autocrossfade to prevent clicks
+- Internal improvements and bugfixing
+
+
+0.1.5 --- 2011 . 03 . 10
+- decimal bpm adjustment
+- ability to shrink/expand actions when changing the global beats
+- improved GUI for beats and bpm controllers
+- improved routines for action management
+- actions are now updated when you change bpm
+
+
+0.1.4 --- 2011 . 03 . 04
+- ability to save recorded actions
+- status box now shows if a recorded chan is deactivated
+- recorder is reset correctly when you load a new patch
+- minor improvements
+
+
+0.1.3 --- 2011 . 02 . 26
+- action recorder (first implementation)
+- quantization procedure slightly optimized
+- minor graphical adjustments
+- expanded documentation
+
+
+0.1.2 --- 2011 . 02 . 08
+- master volume controller
+- improved sound meter with more accuracy
+- improved verifications when reading or writing a patch
+- beat counter is now always reset to 1 after a patch is loaded
+- made loading wave files more robust, plus memory optimizations
+- minor crashes fixed
+
+
+0.1.1 --- 2011 . 01 . 26
+- expansion to 32 channels
+- GUI restyling
+- live quantizer
+- fixed wrong handling of "mute" value when loading a patch
+- minor internal improvements
+
+
+0.1.0 --- 2011 . 01 . 18
+- ability to mute channels
+- stop and rewind buttons now affect only channels in loop mode
+- undo for ending loops
+- internal patch improvements to provide backward compatibility
+- better behaviour when exceeding the total amount of available memory
+- fixed random reversals of stereo field at the end of the beat bar
+- fixed a potential segmentation fault when freeing a sample
+
+
+0.0.12 --- 2011 . 01 . 11
+- ability to free a channel
+- "stop" button to suspend the general program
+- new "stop-to-end" mode for looped channels
+- new "full stop" key combination
+- enhanced mouse interaction
+- minor bugfixing
+
+
+0.0.11 --- 2010 . 12 . 28
+- customizable keys
+- GUI layer optimizations and improvements
+- overwrite confirmation when saving a patch
+- the browser always displays the patch folder when loading a new patch
+- browser url is now read-only to prevent manipulations
+
+
+0.0.10 --- 2010 . 12 . 16
+- new "single-mode retrig" mode added
+- expansion to 16 channels
+- new advanced file browser with the ability to navigate the filesystem
+- audio configuration now uses the "default" device, if not changed
+- graphical restyling for audio channels
+- fixed a random crash on startup, due to a wrong thread synch
+
+
+0.0.9 --- 2010 . 12 . 08
+- new loop once mode
+- new graphical beat meter
+- rewind-program button added
+- heavy buttons and controls restyling
+- reinforced header verification when a new patch is opened for reading
+- some bugfixing for the loading procedure of a patch
+- fixed a potential crash while a new sample is being loaded
+
+
+0.0.8 --- 2010 . 11 . 28
+- fixed a critical crash while loading a sample
+- GUI warning when loading a sample or a patch into an active channel
+- little optimization during the search for data into waves
+- all popup windows are now modal (always on top)
+- fixed a potential crash in case of malformed wave files
+
+
+0.0.7 --- 2010 . 11 . 18
+- new peak meter with clip warning and system status report
+- any "ok" button is associated to the "return" key (for fast inputs)
+- graphical improvements for checkboxes, buttons, smaller fonts in browsers
+- graphical feedback for missing samples
+- internal optimizations
+
+
+0.0.6 --- 2010 . 11 . 01
+- new 32 bit floating point audio engine
+- support for any wave bit-rate, from 8 bit pcm to 32 float
+- Giada now prompts when a sound card error occurs
+- removed the hard-limiting system, now useless
+- the "save patch" panel now shows the actual patchname in use
+- alphabetic sort into the file browser
+- fixed an annoying gui flickering
+- patch volume information are now handled correctly
+- minor internal optimizations
+- fixed a memory leak when loading a new patch
+- other memory optimizations
+
+
+0.0.5 --- 2010 . 10 . 21
+- Patch-based system: load/save your setup from/to a binary file
+- New audio configuration panel
+- New configuration file (giada.conf) where to store data
+- Complete implementation of the double click startup
+- Fixed a bug related to the confirm-on-quit window
+- Minor GUI beautifications
+- Extended documentation
+
+
+0.0.4 --- 2010 . 10 . 11
+- New internal sample-accurate loop engine
+- Ability to configure the period size through ini file
+- First implementation of the double click startup
+- Debug information are now properly tagged, reporting the interested layer
+
+
+0.0.3 --- 2010 . 10 . 02
+- (giada) New official logo
+- (giada) Ability to load single-channel samples
+- (giada) Capital letter consistency between GUI buttons
+- (giada) Added "cancel" button to the browser window
+- (giada) Endianness verification
+- (giada) Cleanup of the audio initialization procedure
+- (giada) Several internal optimization for audio playback
+- (giada) ALSA layer now tells if an underrun occurs
+- (giada) Internal memory allocation improvements
+- (giada) Fixed an unallocated hardware parameter into ALSA configuration
+- (wa) Information about wave endianness
+- Added a "Requirements" section to the readme file
+
+
+0.0.2 --- 2010 . 09 . 17
+- (giada) More visual feedbacks if a key is pressed
+- (giada) Added a graphical alert if a sample is in an incorrect format
+- (giada) Confirm on exit
+- (giada) Graphical improvements for the browser window
+- (giada) Browser window doesn't close itself anymore if a sample format is incorrect
+- (giada) Added "-- no sample --" for empty channels
+- (giada) Startup no longer fails if a sample from the ini file is not found
+- (giada) Internal optimization for the sample loading routine
+- (giada) More graphical consistency between subwindows
+- (giada) The sample name is now truncated to fit into its box, preventing overflow
+- (giada) Other minor GUI tweaks
+- (giada) Internal memory improvements to prevent a bad bug of allocation with malformed wave files
+- (wa) More information about sample size
+- (wa) Added calculations and comparison between data sizes
+
+
+0.0.1 --- 2010 . 09 . 06
+(initial release)
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..f2562da
--- /dev/null
+++ b/README.md
@@ -0,0 +1,74 @@
+<p align="center">
+       <img src="https://raw.githubusercontent.com/monocasual/giada/master/extras/giada-logotype.png" alt="Giada - Your Hardcore Loop Machine">
+</p>
+
+<p align="center">
+<strong>Giada - Your Hardcore Loop Machine</strong> | Official website: <a href="https://www.giadamusic.com">giadamusic.com</a> | <a href="https://github.com/monocasual/giada/actions?query=workflow%3A%22Continuous+integration%22"><img src="https://github.com/monocasual/giada/workflows/Continuous%20integration/badge.svg" alt="Build status"></a>
+</p>
+
+## What is Giada?
+
+Giada is an open source, minimalistic and hardcore music production tool. Designed for DJs, live performers and electronic musicians.
+
+<p align="center">
+✦✦✦ <a href="http://www.youtube.com/user/GiadaLoopMachine">See Giada in action!</a> âœ¦âœ¦âœ¦
+</p>
+
+![Giada Loop Machine screenshot](https://giadamusic.com/images/giada-canvas.png)
+
+## Main features
+
+* Your sample player! Load samples from your crates and play them with a computer keyboard or a MIDI controller;
+* Your loop machine! Build your performance in real time by layering audio tracks or MIDI events, driven by the main sequencer;
+* Your song editor! Write songs from scratch or edit existing live recordings with the powerful Action Editor, for a fine-tuned control;
+* Your live recorder! Record sounds from the real world and MIDI events coming from external devices or other apps;
+* Your FX processor! Process samples or audio/MIDI input signals with VST instruments from your plug-ins collection;
+* Your MIDI controller! Control other software or synchronize physical MIDI devices by using Giada as a MIDI master sequencer.
+
+### And more:
+
+* Ultra-lightweight internal design;
+* multi-thread/multi-core support;
+* 32-bit floating point audio engine;
+* ALSA, JACK + Transport, CoreAudio, ASIO and DirectSound full support;
+* unlimited number of channels (optionally controllable via computer keyboard);
+* BPM and beat sync with sample-accurate loop engine;
+* MIDI input and output support, featuring custom [MIDI lightning messages](https://github.com/monocasual/giada-midimaps);
+* super-sleek, built-in Wave Editor for audio samples and Piano Roll editor for MIDI messages;
+* automatic quantizer;
+* portable project storage system, based on super-hackable JSON files;
+* support for all major uncompressed file formats;
+* test-driven development style supported by [GitHub Actions](https://github.com/monocasual/giada/actions) and [Catch](https://github.com/philsquared/Catch)
+* under a constant stage of development;
+* 100% open-source GPL v3.
+
+## License
+
+Giada is available under the terms of the GNU General Public License.
+Take a look at the COPYING file for further information.
+
+## Documentation
+
+Documentation is available online in the [user guide page](https://www.giadamusic.com/documentation-index).
+
+An ever-growing collection of tutorials (both text and video) and live demos is available in the [tutorials & media page](https://www.giadamusic.com/media).
+
+Found a typo or a terrible mistake? Feel free to clone the [website repository](https://github.com/monocasual/giada-www) and send us your pull requests.
+
+## Build Giada from source
+
+We do our best to make the compilation process as simple as possible. You can find all the information in the [compiling from source](https://www.giadamusic.com/documentation-compiling-from-source) chapter from the user guide.
+
+## Bugs, requests and questions for non-developers
+
+Feel free to ask anything in the [discussions area](https://github.com/monocasual/giada/discussions).
+
+## Copyright
+
+Giada is Copyright (C) 2010-2022 by Giovanni A. Zuliani | Monocasual Laboratories
+
+Giada - Your Hardcore Loopmachine 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.
+
+Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see <http://www.gnu.org/licenses/>.
diff --git a/extras/com.giadamusic.Giada.desktop b/extras/com.giadamusic.Giada.desktop
new file mode 100644 (file)
index 0000000..b9d33b5
--- /dev/null
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=Giada
+Name[es]=Giada
+GenericName=Drum machine and loop sequencer
+GenericName[es]=Caja de ritmos y secuenciador de loops
+Exec=giada %f
+Terminal=false
+Icon=com.giadamusic.Giada
+Categories=Music;AudioVideo;Audio;Midi;X-Digital_Processing;X-Jack;X-MIDI;
+Keywords=Giada;
diff --git a/extras/com.giadamusic.Giada.metainfo.xml b/extras/com.giadamusic.Giada.metainfo.xml
new file mode 100644 (file)
index 0000000..0786ef0
--- /dev/null
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2010-2021 Giovanni A. Zuliani | Monocasual Laboratories -->
+<component type="desktop">
+ <id>com.giadamusic.Giada</id>
+ <metadata_license>CC0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <name>Giada</name>
+ <summary>Your hardcore loop machine</summary>
+ <description>
+   <p>
+     Giada is an open source, minimalistic and hardcore music
+     production tool. Designed for DJs, live performers and electronic
+     musicians.
+   </p>
+ </description>
+ <content_rating type="oars-1.1" />
+ <launchable type="desktop-id">com.giadamusic.Giada.desktop</launchable>
+ <screenshots>
+  <screenshot type="default">
+    <image>
+      https://www.giadamusic.com/images/screenshots/giada-loop-machine-screenshot-03-large-project.png
+    </image>
+   <caption>A fairly large project with samples and MIDI events</caption>
+  </screenshot>
+  <screenshot>
+    <image>
+      https://www.giadamusic.com/images/screenshots/giada-loop-machine-screenshot-17-midi-action-editor.png
+    </image>
+   <caption>New Action Editor for MIDI events</caption>
+  </screenshot>
+  <screenshot>
+    <image>
+      https://www.giadamusic.com/images/screenshots/giada-loop-machine-screenshot-01-sample-editor.png
+    </image>
+   <caption>Chopping samples in the advanced Sample Editor</caption>
+  </screenshot>
+ </screenshots>
+ <update_contact>giadaloopmachine_AT_gmail.com</update_contact>
+ <url type="homepage">https://www.giadamusic.com/</url>
+ <url type="help">https://www.giadamusic.com/forum</url>
+ <releases>
+   <release version="0.18.0" date="2021-05-19">
+     <description>
+       <ul>
+         <li>New 'free loop-length' audio recording mode (#63)</li>
+         <li>Many AudioBuffer improvements</li>
+         <li>Audio configuration panel refactoring</li>
+         <li>KernelAudio improvements and cleanups</li>
+         <li>Relaxed BPM handling when working with JACK</li>
+         <li>Install executable to FHS compliant location (#450)</li>
+         <li>[CI] Don't UPX binaries on macOS (#459)</li>
+         <li>Fix Overdub protection ON by default not working (#460)</li>
+         <li>Fix crash when moving up from a deleted folder (#455)</li>
+       </ul>
+     </description>
+   </release>
+ </releases>
+</component>
diff --git a/extras/giada-logo.png b/extras/giada-logo.png
new file mode 100644 (file)
index 0000000..66b17f3
Binary files /dev/null and b/extras/giada-logo.png differ
diff --git a/extras/giada-logo.svg b/extras/giada-logo.svg
new file mode 100644 (file)
index 0000000..17e6885
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Creator: CorelDRAW X7 -->
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" style="image-rendering:optimizeQuality;text-rendering:geometricPrecision;shape-rendering:geometricPrecision" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" xml:space="preserve" height="1.557in" width="1.557in" version="1.1" clip-rule="evenodd" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1557.0001 1556.9888" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><defs><style type="text/css"><![CDATA[
+    .str0 {stroke:#D2D3D5;stroke-width:6.94488}
+    .fil2 {fill:#FEFEFE}
+    .fil0 {fill:#FEFEFE}
+    .fil11 {fill:#E6E7E8}
+    .fil5 {fill:#4B4B4D}
+    .fil1 {fill:#454545}
+    .fil4 {fill:#E01C26}
+    .fil10 {fill:#E01C26}
+    .fil7 {fill:url(#id0)}
+    .fil8 {fill:url(#id1)}
+    .fil3 {fill:url(#id2)}
+    .fil6 {fill:url(#id3)}
+    .fil9 {fill:url(#id4)}
+   ]]></style><linearGradient id="id0" y2="14445" gradientUnits="userSpaceOnUse" x2="11848" y1="14445" x1="10647"><stop stop-color="#FEFEFE" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient><linearGradient id="id1" y2="12968" gradientUnits="userSpaceOnUse" x2="2744.5" y1="12032" x1="2042.5"><stop stop-color="#E01C26" offset="0"/><stop stop-color="#822F2F" offset="1"/></linearGradient><linearGradient id="id2" y2="10789" xlink:href="#id1" gradientUnits="userSpaceOnUse" x2="11599" y1="9853.5" x1="10897"/><linearGradient id="id3" y2="14913" xlink:href="#id1" gradientUnits="userSpaceOnUse" x2="11599" y1="13978" x1="10897"/><linearGradient id="id4" y2="22898" gradientUnits="userSpaceOnUse" x2="2500.9" y1="17663" x1="7754.9"><stop stop-color="#E01C26" offset="0"/><stop stop-color="#993132" offset="1"/></linearGradient></defs><g transform="translate(-1615 -3941)"><path d="m2394 3941c430 0 778 348 778 779 0 430-348 778-778 778-431 0-779-348-779-778 0-431 348-779 779-779z" class="fil1" fill="#454545"/><path d="m2394 4362c197 0 357 160 357 358v2h11c91 0 171 43 222 110 7-36 10-74 10-112 0-332-269-601-600-601-332 0-601 269-601 601 0 331 269 600 601 600 89 0 175-20 251-55 27 10 56 15 86 15h17c126-9 227-114 227-243 0-48-14-92-37-130-44-68-120-114-207-114-135 0-244 109-244 244 0 9 0 18 1 27-30 8-62 13-94 13-198 0-358-160-358-357 0-198 160-358 358-358zm151 263c-32-50-88-84-151-84-99 0-179 80-179 179 0 98 80 178 179 178 4 0 8 0 12-1 93-6 166-83 166-177 0-35-10-68-27-95z" class="fil2" fill="#fefefe"/></g></svg>
diff --git a/extras/giada-logotype.png b/extras/giada-logotype.png
new file mode 100644 (file)
index 0000000..f9a4c02
Binary files /dev/null and b/extras/giada-logotype.png differ
diff --git a/extras/giada.icns b/extras/giada.icns
new file mode 100644 (file)
index 0000000..627e605
Binary files /dev/null and b/extras/giada.icns differ
diff --git a/scripts/create_source_tarball.sh b/scripts/create_source_tarball.sh
new file mode 100755 (executable)
index 0000000..aff5570
--- /dev/null
@@ -0,0 +1,130 @@
+#!/usr/bin/env bash
+#
+# Creates source tarballs for giada in the form of
+# 'giada-x.x.x-src.tar.gz' and optionally detached PGP signatures
+# for the created file of the form 'giada-x.x.x-src.tar.gz.asc'.
+# If the environment variable BUILD_DIR is provided, the files will be moved to
+# $BUILD_DIR/, else to the location of this script (the repository folder).
+#
+# Requirements:
+# - git
+# - tar
+# - a writable (user) /tmp folder for mktemp
+# - gnupg >= 2.0.0 (if source tarball signing is requested)
+# - a valid PGP signing key in the keyring (if source tarball signing is
+# requested)
+
+set -euo pipefail
+
+get_absolute_path() {
+    cd "$(dirname "$1")" && pwd -P
+}
+
+validate_project_tag() {
+    if ! git ls-remote -t "${upstream}"| grep -e "${version}$" > /dev/null; then
+        echo "The tag '$version' could not be found in upstream repository (${upstream})."
+        exit 1
+    fi
+}
+
+checkout_project() {
+    echo "Cloning project below working directory ${working_dir}"
+    cd "$working_dir"
+    git clone "$upstream" --branch "$version" \
+                          --single-branch \
+                          --depth=1 \
+                          --recurse-submodules \
+                          --shallow-submodules \
+                          "${output_name}"
+}
+
+clean_sources() {
+    cd "${working_dir}/${output_name}"
+    echo "Removing unneeded files and folders..."
+    rm -rfv .git* \
+            .travis* \
+            create_source_tarball.sh
+}
+
+compress_sources() {
+    cd "${working_dir}"
+    tar cvfz "${output_name}.tar.gz" "${output_name}"
+}
+
+move_sources() {
+    cd "${working_dir}"
+    mv -v "${output_name}.tar.gz" "${output_dir}/"
+}
+
+sign_sources() {
+    cd "${output_dir}"
+    gpg --detach-sign \
+        -u "${signer}" \
+        -o "${output_name}.tar.gz.asc" \
+        "${output_name}.tar.gz"
+}
+
+cleanup_working_dir() {
+    echo "Removing working directory: ${working_dir}"
+    rm -rf "${working_dir}"
+}
+
+print_help() {
+    echo "Usage: $0 -v <version tag> -s <signature email or key ID>"
+    exit 1
+}
+
+if [ -n "${BUILD_DIR:-}" ]; then
+    echo "Build dir provided: ${BUILD_DIR}"
+    output_dir="${BUILD_DIR}/"
+    mkdir -p "${output_dir}"
+else
+    output_dir="$(get_absolute_path "$0")"
+fi
+
+upstream="https://github.com/monocasual/giada"
+package_name="giada"
+working_dir="$(mktemp -d)"
+version="$(date '+%Y-%m-%d')"
+output_version=""
+output_name=""
+signer=""
+signature=0
+
+# remove the working directory, no matter what
+trap cleanup_working_dir EXIT
+
+if [ ${#@} -gt 0 ]; then
+    while getopts 'hv:s:' flag; do
+        case "${flag}" in
+            h) print_help
+                ;;
+            s) signer=$OPTARG
+                signature=1
+                ;;
+            v) version=$OPTARG
+                output_version="${version//v}"
+                ;;
+            *)
+                echo "Error! Try '${0} -h'."
+                exit 1
+                ;;
+        esac
+    done
+else
+    print_help
+fi
+
+output_name="${package_name}-${output_version}-src"
+validate_project_tag
+checkout_project
+clean_sources
+compress_sources
+move_sources
+if [ $signature -eq 1 ]; then
+    sign_sources
+fi
+
+exit 0
+
+# vim:set ts=4 sw=4 et:
diff --git a/src/core/actions/action.h b/src/core/actions/action.h
new file mode 100644 (file)
index 0000000..ba960b8
--- /dev/null
@@ -0,0 +1,61 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_ACTION_H
+#define G_ACTION_H
+
+#include "src/core/midiEvent.h"
+#include "src/core/types.h"
+
+namespace giada::m
+{
+struct Action
+{
+       ID        id = 0; // Invalid
+       ID        channelId;
+       Frame     frame;
+       MidiEvent event;
+       ID        pluginId    = -1;
+       int       pluginParam = -1;
+       ID        prevId      = 0;
+       ID        nextId      = 0;
+
+       const Action* prev = nullptr;
+       const Action* next = nullptr;
+
+       bool isValid() const
+       {
+               return id != 0;
+       }
+
+       bool isVolumeEnvelope() const
+       {
+               return event.getStatus() == MidiEvent::ENVELOPE && pluginId == -1;
+       }
+};
+} // namespace giada::m
+
+#endif
\ No newline at end of file
diff --git a/src/core/actions/actionRecorder.cpp b/src/core/actions/actionRecorder.cpp
new file mode 100644 (file)
index 0000000..12cdaa4
--- /dev/null
@@ -0,0 +1,355 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/actions/actionRecorder.h"
+#include "core/actions/action.h"
+#include "core/actions/actions.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "core/patch.h"
+#include "utils/log.h"
+#include "utils/ver.h"
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+#include <cstddef>
+#include <unordered_map>
+
+namespace giada::m
+{
+namespace
+{
+constexpr int MAX_LIVE_RECS_CHUNK = 128;
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+ActionRecorder::ActionRecorder(model::Model& m)
+: m_model(m)
+, m_actions(m)
+{
+       m_liveActions.reserve(MAX_LIVE_RECS_CHUNK);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void ActionRecorder::reset()
+{
+       m_liveActions.clear();
+       m_actions.reset();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool ActionRecorder::isBoundaryEnvelopeAction(const Action& a) const
+{
+       assert(a.prev != nullptr);
+       assert(a.next != nullptr);
+       return a.prev->frame > a.frame || a.next->frame < a.frame;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void ActionRecorder::updateBpm(float ratio, int quantizerStep)
+{
+       if (ratio == 1.0f)
+               return;
+
+       m_actions.updateKeyFrames([=](Frame old) {
+               /* The division here cannot be precise. A new frame can be 44099 and the 
+               quantizer set to 44100. That would mean two recs completely useless. So we 
+               compute a reject value ('delta'): if it's lower than 6 frames the new frame 
+               is collapsed with a quantized frame. FIXME - maybe 6 frames are too low. */
+               Frame frame = static_cast<Frame>(old * ratio);
+               if (frame != 0)
+               {
+                       Frame delta = quantizerStep % frame;
+                       if (delta > 0 && delta <= 6)
+                               frame = frame + delta;
+               }
+               return frame;
+       });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void ActionRecorder::updateSamplerate(int systemRate, int patchRate)
+{
+       if (systemRate == patchRate)
+               return;
+
+       float ratio = systemRate / (float)patchRate;
+
+       m_actions.updateKeyFrames([=](Frame old) { return floorf(old * ratio); });
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool ActionRecorder::cloneActions(ID channelId, ID newChannelId)
+{
+       bool                       cloned = false;
+       std::vector<Action>        actions;
+       std::unordered_map<ID, ID> map; // Action ID mapper, old -> new
+
+       m_actions.forEachAction([&](const Action& a) {
+               if (a.channelId != channelId)
+                       return;
+
+               ID newActionId = m_actions.getNewActionId();
+
+               map.insert({a.id, newActionId});
+
+               Action clone(a);
+               clone.id        = newActionId;
+               clone.channelId = newChannelId;
+
+               actions.push_back(clone);
+               cloned = true;
+       });
+
+       /* Update nextId and prevId relationships given the new action ID. */
+
+       for (Action& a : actions)
+       {
+               if (a.prevId != 0)
+                       a.prevId = map.at(a.prevId);
+               if (a.nextId != 0)
+                       a.nextId = map.at(a.nextId);
+       }
+
+       m_actions.rec(actions);
+
+       return cloned;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void ActionRecorder::liveRec(ID channelId, MidiEvent e, Frame globalFrame)
+{
+       assert(e.isNoteOnOff()); // Can't record any other kind of events for now
+
+       /* TODO - this might allocate on the MIDI thread */
+       if (m_liveActions.size() >= m_liveActions.capacity())
+               m_liveActions.reserve(m_liveActions.size() + MAX_LIVE_RECS_CHUNK);
+
+       m_liveActions.push_back(m_actions.makeAction(m_actions.getNewActionId(), channelId, globalFrame, e));
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unordered_set<ID> ActionRecorder::consolidate()
+{
+       for (auto it = m_liveActions.begin(); it != m_liveActions.end(); ++it)
+               consolidate(*it, it - m_liveActions.begin()); // Pass current index
+
+       m_actions.rec(m_liveActions);
+
+       std::unordered_set<ID> out;
+       for (const Action& action : m_liveActions)
+               out.insert(action.channelId);
+
+       m_liveActions.clear();
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void ActionRecorder::clearAllActions()
+{
+       for (Channel& ch : m_model.get().channels)
+               ch.hasActions = false;
+       m_model.swap(model::SwapType::HARD);
+
+       m_actions.clearAll();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Actions::Map ActionRecorder::deserializeActions(const std::vector<Patch::Action>& pactions)
+{
+       Actions::Map out;
+
+       /* First pass: add actions with no relationship, that is with no prev/next
+       pointers filled in. */
+
+       for (const Patch::Action& paction : pactions)
+               out[paction.frame].push_back(m_actions.makeAction(paction));
+
+       /* Second pass: fill in previous and next actions, if any. Is this the
+       fastest/smartest way to do it? Maybe not. Optimizations are welcome. */
+
+       for (const Patch::Action& paction : pactions)
+       {
+               if (paction.nextId == 0 && paction.prevId == 0)
+                       continue;
+               Action* curr = const_cast<Action*>(getActionPtrById(paction.id, out));
+               assert(curr != nullptr);
+               if (paction.nextId != 0)
+               {
+                       curr->next = getActionPtrById(paction.nextId, out);
+                       assert(curr->next != nullptr);
+               }
+               if (paction.prevId != 0)
+               {
+                       curr->prev = getActionPtrById(paction.prevId, out);
+                       assert(curr->prev != nullptr);
+               }
+       }
+
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<Patch::Action> ActionRecorder::serializeActions(const Actions::Map& actions)
+{
+       std::vector<Patch::Action> out;
+       for (const auto& kv : actions)
+       {
+               for (const Action& a : kv.second)
+               {
+                       out.push_back({
+                           a.id,
+                           a.channelId,
+                           a.frame,
+                           a.event.getRaw(),
+                           a.prevId,
+                           a.nextId,
+                       });
+               }
+       }
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+const Action* ActionRecorder::getActionPtrById(int id, const Actions::Map& source)
+{
+       for (const auto& [_, actions] : source)
+               for (const Action& action : actions)
+                       if (action.id == id)
+                               return &action;
+       return nullptr;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool ActionRecorder::areComposite(const Action& a1, const Action& a2) const
+{
+       return a1.event.getStatus() == MidiEvent::NOTE_ON &&
+              a2.event.getStatus() == MidiEvent::NOTE_OFF &&
+              a1.event.getNote() == a2.event.getNote() &&
+              a1.channelId == a2.channelId;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void ActionRecorder::consolidate(const Action& a1, std::size_t i)
+{
+       /* This algorithm must start searching from the element next to 'a1': since 
+       live actions are recorded in linear sequence, the potential partner of 'a1' 
+       always lies beyond a1 itself. Without this trick (i.e. if it loops from 
+       vector.begin() each time) the algorithm would end up matching wrong partners. */
+
+       for (auto it = m_liveActions.begin() + i; it != m_liveActions.end(); ++it)
+       {
+
+               const Action& a2 = *it;
+
+               if (!areComposite(a1, a2))
+                       continue;
+
+               const_cast<Action&>(a1).nextId = a2.id;
+               const_cast<Action&>(a2).prevId = a1.id;
+
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+const std::vector<Action>* ActionRecorder::getActionsOnFrame(Frame f) const
+{
+       return m_actions.getActionsOnFrame(f);
+}
+
+bool ActionRecorder::hasActions(ID channelId, int type) const
+{
+       return m_actions.hasActions(channelId, type);
+}
+
+Action ActionRecorder::getClosestAction(ID channelId, Frame f, int type) const
+{
+       return m_actions.getClosestAction(channelId, f, type);
+}
+
+std::vector<Action> ActionRecorder::getActionsOnChannel(ID channelId) const
+{
+       return m_actions.getActionsOnChannel(channelId);
+}
+
+void ActionRecorder::clearChannel(ID channelId)
+{
+       m_actions.clearChannel(channelId);
+}
+
+void ActionRecorder::clearActions(ID channelId, int type)
+{
+       m_actions.clearActions(channelId, type);
+}
+
+Action ActionRecorder::rec(ID channelId, Frame frame, MidiEvent e)
+{
+       return m_actions.rec(channelId, frame, e);
+}
+
+void ActionRecorder::rec(ID channelId, Frame f1, Frame f2, MidiEvent e1, MidiEvent e2)
+{
+       return m_actions.rec(channelId, f1, f2, e1, e2);
+}
+
+void ActionRecorder::updateSiblings(ID id, ID prevId, ID nextId)
+{
+       m_actions.updateSiblings(id, prevId, nextId);
+}
+
+void ActionRecorder::deleteAction(ID id)
+{
+       m_actions.deleteAction(id);
+}
+
+void ActionRecorder::deleteAction(ID currId, ID nextId)
+{
+       m_actions.deleteAction(currId, nextId);
+}
+
+void ActionRecorder::updateEvent(ID id, MidiEvent e)
+{
+       m_actions.updateEvent(id, e);
+}
+} // namespace giada::m
diff --git a/src/core/actions/actionRecorder.h b/src/core/actions/actionRecorder.h
new file mode 100644 (file)
index 0000000..157dc05
--- /dev/null
@@ -0,0 +1,131 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_ACTION_RECORDER_H
+#define G_ACTION_RECORDER_H
+
+#include "core/actions/actions.h"
+#include "core/midiEvent.h"
+#include "core/types.h"
+#include <cstddef>
+#include <unordered_set>
+
+namespace giada::m::patch
+{
+struct Action;
+}
+
+namespace giada::m
+{
+struct Action;
+class ActionRecorder
+{
+public:
+       ActionRecorder(model::Model&);
+
+       /* reset
+       Brings everything back to the initial state. */
+
+       void reset();
+
+       bool isBoundaryEnvelopeAction(const Action& a) const;
+
+       /* updateBpm
+    Changes actions position by calculating the new bpm value. */
+
+       void updateBpm(float ratio, int quantizerStep);
+
+       /* updateSamplerate
+    Changes actions position by taking in account the new samplerate. If 
+    f_system == f_patch nothing will change, otherwise the conversion is 
+    mandatory. */
+
+       void updateSamplerate(int systemRate, int patchRate);
+
+       /* cloneActions
+    Clones actions in channel 'channelId', giving them a new channel ID. Returns
+    whether any action has been cloned. */
+
+       bool cloneActions(ID channelId, ID newChannelId);
+
+       /* liveRec
+    Records a user-generated action. NOTE_ON or NOTE_OFF only for now. */
+
+       void liveRec(ID channelId, MidiEvent e, Frame global);
+
+       /* consolidate
+    Records all live actions. Returns a set of channels IDs that have been 
+    recorded. */
+
+       std::unordered_set<ID> consolidate();
+
+       /* clearAllActions
+    Deletes all recorded actions. */
+
+       void clearAllActions();
+
+       /* (de)serializeActions
+    Creates new Actions given the patch raw data and vice versa. */
+
+       Actions::Map               deserializeActions(const std::vector<Patch::Action>& as);
+       std::vector<Patch::Action> serializeActions(const Actions::Map& as);
+
+       /* Pass-thru functions. See Actions.h */
+
+       const std::vector<Action>* getActionsOnFrame(Frame f) const;
+       bool                       hasActions(ID channelId, int type = 0) const;
+       Action                     getClosestAction(ID channelId, Frame f, int type) const;
+       std::vector<Action>        getActionsOnChannel(ID channelId) const;
+       void                       clearChannel(ID channelId);
+       void                       clearActions(ID channelId, int type);
+       Action                     rec(ID channelId, Frame frame, MidiEvent e);
+       void                       rec(ID channelId, Frame f1, Frame f2, MidiEvent e1, MidiEvent e2);
+       void                       updateSiblings(ID id, ID prevId, ID nextId);
+       void                       deleteAction(ID id);
+       void                       deleteAction(ID currId, ID nextId);
+       void                       updateEvent(ID id, MidiEvent e);
+
+private:
+       /* areComposite
+    Composite: NOTE_ON + NOTE_OFF on the same note. */
+
+       bool areComposite(const Action& a1, const Action& a2) const;
+
+       const Action* getActionPtrById(int id, const Actions::Map& source);
+
+       /* consolidate
+    Given an action 'a1' tries to find the matching NOTE_OFF and updates the
+    action accordingly. */
+
+       void consolidate(const Action& a1, std::size_t i);
+
+       model::Model&       m_model;
+       Actions             m_actions;
+       std::vector<Action> m_liveActions;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/actions/actions.cpp b/src/core/actions/actions.cpp
new file mode 100644 (file)
index 0000000..04d1dc9
--- /dev/null
@@ -0,0 +1,352 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "actions.h"
+#include "action.h"
+#include "core/idManager.h"
+#include "core/model/model.h"
+#include "utils/log.h"
+#include <algorithm>
+#include <cassert>
+#include <memory>
+
+namespace giada::m
+{
+Actions::Actions(model::Model& model)
+: m_model(model)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::reset()
+{
+       m_actionId = IdManager();
+       clearAll();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::clearAll()
+{
+       model::DataLock lock = m_model.lockData();
+       m_model.getAllShared<Map>().clear();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::clearChannel(ID channelId)
+{
+       removeIf([=](const Action& a) { return a.channelId == channelId; });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::clearActions(ID channelId, int type)
+{
+       removeIf([=](const Action& a) {
+               return a.channelId == channelId && a.event.getStatus() == type;
+       });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::deleteAction(ID id)
+{
+       removeIf([=](const Action& a) { return a.id == id; });
+}
+
+void Actions::deleteAction(ID currId, ID nextId)
+{
+       removeIf([=](const Action& a) { return a.id == currId || a.id == nextId; });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::updateKeyFrames(std::function<Frame(Frame old)> f)
+{
+       Map temp;
+
+       /* Copy all existing actions in local map by cloning them, with just a
+       difference: they have a new frame value. */
+
+       for (const auto& [oldFrame, actions] : m_model.getAllShared<Map>())
+       {
+               Frame newFrame = f(oldFrame);
+               for (const Action& a : actions)
+               {
+                       Action copy = a;
+                       copy.frame  = newFrame;
+                       temp[newFrame].push_back(copy);
+               }
+               G_DEBUG(oldFrame << " -> " << newFrame);
+       }
+
+       updateMapPointers(temp);
+
+       model::DataLock lock  = m_model.lockData();
+       m_model.getAllShared<Map>() = std::move(temp);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::updateEvent(ID id, MidiEvent e)
+{
+       model::DataLock lock = m_model.lockData();
+
+       findAction(m_model.getAllShared<Map>(), id)->event = e;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::updateSiblings(ID id, ID prevId, ID nextId)
+{
+       model::DataLock lock = m_model.lockData();
+
+       Action* pcurr = findAction(m_model.getAllShared<Map>(), id);
+       Action* pprev = findAction(m_model.getAllShared<Map>(), prevId);
+       Action* pnext = findAction(m_model.getAllShared<Map>(), nextId);
+
+       pcurr->prev   = pprev;
+       pcurr->prevId = pprev->id;
+       pcurr->next   = pnext;
+       pcurr->nextId = pnext->id;
+
+       if (pprev != nullptr)
+       {
+               pprev->next   = pcurr;
+               pprev->nextId = pcurr->id;
+       }
+       if (pnext != nullptr)
+       {
+               pnext->prev   = pcurr;
+               pnext->prevId = pcurr->id;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Actions::hasActions(ID channelId, int type) const
+{
+       for (const auto& [frame, actions] : m_model.getAllShared<Map>())
+               for (const Action& a : actions)
+                       if (a.channelId == channelId && (type == 0 || type == a.event.getStatus()))
+                               return true;
+       return false;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Action Actions::makeAction(ID id, ID channelId, Frame frame, MidiEvent e)
+{
+       Action out{m_actionId.generate(id), channelId, frame, e, -1, -1};
+       m_actionId.set(id);
+       return out;
+}
+
+Action Actions::makeAction(const Patch::Action& a)
+{
+       m_actionId.set(a.id);
+       return Action{a.id, a.channelId, a.frame, a.event, -1, -1, a.prevId,
+           a.nextId};
+}
+
+/* -------------------------------------------------------------------------- */
+
+Action Actions::rec(ID channelId, Frame frame, MidiEvent event)
+{
+       /* Skip duplicates. */
+
+       if (exists(channelId, frame, event))
+               return {};
+
+       Action a = makeAction(0, channelId, frame, event);
+
+       /* If key frame doesn't exist yet, the [] operator in std::map is smart 
+       enough to insert a new item first. No plug-in data for now. */
+
+       model::DataLock lock = m_model.lockData();
+
+       m_model.getAllShared<Map>()[frame].push_back(a);
+       updateMapPointers(m_model.getAllShared<Map>());
+
+       return a;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::rec(std::vector<Action>& actions)
+{
+       if (actions.size() == 0)
+               return;
+
+       model::DataLock lock = m_model.lockData();
+
+       Map& map = m_model.getAllShared<Map>();
+
+       for (const Action& a : actions)
+               if (!exists(a.channelId, a.frame, a.event, map))
+                       map[a.frame].push_back(a);
+       updateMapPointers(map);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::rec(ID channelId, Frame f1, Frame f2, MidiEvent e1, MidiEvent e2)
+{
+       model::DataLock lock = m_model.lockData();
+
+       Map& map = m_model.getAllShared<Map>();
+
+       map[f1].push_back(makeAction(0, channelId, f1, e1));
+       map[f2].push_back(makeAction(0, channelId, f2, e2));
+
+       Action* a1 = findAction(map, map[f1].back().id);
+       Action* a2 = findAction(map, map[f2].back().id);
+       a1->nextId = a2->id;
+       a2->prevId = a1->id;
+
+       updateMapPointers(map);
+}
+
+/* -------------------------------------------------------------------------- */
+
+const std::vector<Action>* Actions::getActionsOnFrame(Frame frame) const
+{
+       if (m_model.getAllShared<Map>().count(frame) == 0)
+               return nullptr;
+       return &m_model.getAllShared<Map>().at(frame);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Action Actions::getClosestAction(ID channelId, Frame f, int type) const
+{
+       Action out = {};
+       forEachAction([&](const Action& a) {
+               if (a.event.getStatus() != type || a.channelId != channelId)
+                       return;
+               if (!out.isValid() || (a.frame <= f && a.frame > out.frame))
+                       out = a;
+       });
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<Action> Actions::getActionsOnChannel(ID channelId) const
+{
+       std::vector<Action> out;
+       forEachAction([&](const Action& a) {
+               if (a.channelId == channelId)
+                       out.push_back(a);
+       });
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::forEachAction(std::function<void(const Action&)> f) const
+{
+       for (auto& [_, actions] : m_model.getAllShared<Map>())
+               for (const Action& action : actions)
+                       f(action);
+}
+
+/* -------------------------------------------------------------------------- */
+
+ID Actions::getNewActionId()
+{
+       return m_actionId.generate();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Action* Actions::findAction(Map& src, ID id)
+{
+       for (auto& [frame, actions] : src)
+               for (Action& a : actions)
+                       if (a.id == id)
+                               return &a;
+       assert(false);
+       return nullptr;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::updateMapPointers(Map& src)
+{
+       for (auto& kv : src)
+       {
+               for (Action& action : kv.second)
+               {
+                       if (action.nextId != 0)
+                               action.next = findAction(src, action.nextId);
+                       if (action.prevId != 0)
+                               action.prev = findAction(src, action.prevId);
+               }
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::optimize(Map& map)
+{
+       for (auto it = map.cbegin(); it != map.cend();)
+               it->second.size() == 0 ? it = map.erase(it) : ++it;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Actions::removeIf(std::function<bool(const Action&)> f)
+{
+       model::DataLock lock = m_model.lockData();
+
+       Map& map = m_model.getAllShared<Map>();
+       for (auto& [frame, actions] : map)
+               actions.erase(std::remove_if(actions.begin(), actions.end(), f), actions.end());
+       optimize(map);
+       updateMapPointers(map);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Actions::exists(ID channelId, Frame frame, const MidiEvent& event, const Map& target) const
+{
+       for (const auto& [_, actions] : target)
+               for (const Action& a : actions)
+                       if (a.channelId == channelId && a.frame == frame && a.event.getRaw() == event.getRaw())
+                               return true;
+       return false;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Actions::exists(ID channelId, Frame frame, const MidiEvent& event) const
+{
+       return exists(channelId, frame, event, m_model.getAllShared<Map>());
+}
+} // namespace giada::m
diff --git a/src/core/actions/actions.h b/src/core/actions/actions.h
new file mode 100644 (file)
index 0000000..d098eae
--- /dev/null
@@ -0,0 +1,185 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_ACTIONS_H
+#define G_ACTIONS_H
+
+#include "action.h"
+#include "core/idManager.h"
+#include "core/midiEvent.h"
+#include "core/patch.h"
+#include "core/types.h"
+#include <functional>
+#include <map>
+#include <memory>
+#include <vector>
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::m
+{
+class Actions
+{
+public:
+       using Map = std::map<Frame, std::vector<Action>>;
+
+       Actions(model::Model& model);
+
+       /* forEachAction
+    Applies a read-only callback on each action recorded. NEVER do anything
+    inside the callback that might alter the ActionMap. */
+
+       void forEachAction(std::function<void(const Action&)> f) const;
+
+       /* getActionsOnChannel
+    Returns a vector of actions belonging to channel 'ch'. */
+
+       std::vector<Action> getActionsOnChannel(ID channelId) const;
+
+       /* getClosestAction
+    Given a frame 'f' returns the closest action. */
+
+       Action getClosestAction(ID channelId, Frame f, int type) const;
+
+       /* getActionsOnFrame
+    Returns a pointer to a vector of actions recorded on frame 'f', or nullptr 
+    if the frame has no actions. */
+
+       const std::vector<Action>* getActionsOnFrame(Frame f) const;
+
+       /* hasActions
+    Checks if the channel has at least one action recorded. */
+
+       bool hasActions(ID channelId, int type = 0) const;
+
+       /* makeAction
+    Makes a new action given some data. */
+       //TODO - move to actionManager
+
+       Action makeAction(ID id, ID channelId, Frame frame, MidiEvent e);
+       Action makeAction(const Patch::Action& a);
+
+       /* reset
+       Brings everything back to the initial state. */
+
+       void reset();
+
+       /* clearAll
+    Deletes all recorded actions. */
+
+       void clearAll();
+
+       /* clearChannel
+    Clears all actions from a channel. */
+
+       void clearChannel(ID channelId);
+
+       /* clearActions
+    Clears the actions by type from a channel. */
+
+       void clearActions(ID channelId, int type);
+
+       /* deleteAction (1)
+    Deletes a specific action. */
+
+       void deleteAction(ID id);
+
+       /* deleteAction (2)
+    Deletes a specific pair of actions. Useful for composite stuff (i.e. MIDI). */
+
+       void deleteAction(ID currId, ID nextId);
+
+       /* updateKeyFrames
+    Update all the key frames in the internal map of actions, according to a 
+    lambda function 'f'. */
+
+       void updateKeyFrames(std::function<Frame(Frame old)> f);
+
+       /* updateEvent
+    Changes the event in action 'a'. */
+
+       void updateEvent(ID id, MidiEvent e);
+
+       /* updateSiblings
+    Changes previous and next actions in action with id 'id'. Mostly used for
+    chained actions such as envelopes. */
+
+       void updateSiblings(ID id, ID prevId, ID nextId);
+
+       /* rec (1)
+    Records an action and returns it. Used by the Action Editor. */
+
+       Action rec(ID channelId, Frame frame, MidiEvent e);
+
+       /* rec (2)
+    Transfer a vector of actions into the current ActionMap. This is called by 
+    recordHandler when a live session is over and consolidation is required. */
+
+       void rec(std::vector<Action>& actions);
+
+       /* rec (3)
+    Records two actions on channel 'channel'. Useful when recording composite 
+    actions in the Action Editor. */
+
+       void rec(ID channelId, Frame f1, Frame f2, MidiEvent e1, MidiEvent e2);
+
+       /* getNewActionId
+    Returns a new action ID, internally generated. */
+       //TODO - move to actionManager
+
+       ID getNewActionId();
+
+private:
+       bool exists(ID channelId, Frame frame, const MidiEvent& event, const Map& target) const;
+       bool exists(ID channelId, Frame frame, const MidiEvent& event) const;
+
+       Action* findAction(Map& src, ID id);
+
+       /* updateMapPointers
+    Updates all prev/next actions pointers into the action map. This is required
+    after an action has been recorded, since pushing back new actions in a Action 
+    vector makes it reallocating the existing ones. */
+
+       void updateMapPointers(Map& src);
+
+       /* optimize
+    Removes frames without actions. */
+
+       void optimize(Map& map);
+
+       void removeIf(std::function<bool(const Action&)> f);
+
+       model::Model& m_model;
+
+       //TODO - move to actionManager
+       IdManager m_actionId;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/audioReceiver.cpp b/src/core/channels/audioReceiver.cpp
new file mode 100644 (file)
index 0000000..bdaf232
--- /dev/null
@@ -0,0 +1,51 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "audioReceiver.h"
+#include "core/channels/channel.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+
+namespace giada::m
+{
+AudioReceiver::AudioReceiver(const Patch::Channel& p)
+: inputMonitor(p.inputMonitor)
+, overdubProtection(p.overdubProtection)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void AudioReceiver::render(const Channel& ch, const mcl::AudioBuffer& in) const
+{
+       /* If armed and input monitor is on, copy input buffer to channel buffer: 
+       this enables the input monitoring. The channel buffer will be overwritten 
+       later on by pluginHost::processStack, so that you would record "clean" audio 
+       (i.e. not plugin-processed). */
+
+       if (ch.armed && inputMonitor)
+               ch.shared->audioBuffer.set(in, /*gain=*/1.0f); // add, don't overwrite
+}
+} // namespace giada::m
diff --git a/src/core/channels/audioReceiver.h b/src/core/channels/audioReceiver.h
new file mode 100644 (file)
index 0000000..eec25bc
--- /dev/null
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_AUDIO_RECEIVER_H
+#define G_CHANNEL_AUDIO_RECEIVER_H
+
+#include "core/patch.h"
+
+namespace mcl
+{
+class AudioBuffer;
+}
+
+namespace giada::m
+{
+class Channel;
+class AudioReceiver final
+{
+public:
+       AudioReceiver() = default;
+       AudioReceiver(const Patch::Channel& p);
+       AudioReceiver(const AudioReceiver& o) = default;
+
+       void render(const Channel& ch, const mcl::AudioBuffer& in) const;
+
+       bool inputMonitor;
+       bool overdubProtection;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/channel.cpp b/src/core/channels/channel.cpp
new file mode 100644 (file)
index 0000000..7cf7081
--- /dev/null
@@ -0,0 +1,468 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/channels/channel.h"
+#include "core/actions/actionRecorder.h"
+#include "core/channels/sampleAdvancer.h"
+#include "core/conf.h"
+#include "core/engine.h"
+#include "core/midiMapper.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/plugins/pluginHost.h"
+#include "core/plugins/pluginManager.h"
+#include "core/recorder.h"
+#include <cassert>
+
+extern giada::m::Engine g_engine;
+
+namespace giada::m
+{
+namespace
+{
+mcl::AudioBuffer::Pan calcPanning_(float pan)
+{
+       /* TODO - precompute the AudioBuffer::Pan when pan value changes instead of
+       building it on the fly. */
+
+       /* Center pan (0.5f)? Pass-through. */
+
+       if (pan == 0.5f)
+               return {1.0f, 1.0f};
+       return {1.0f - pan, pan};
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+ChannelShared::ChannelShared(Frame bufferSize)
+: audioBuffer(bufferSize, G_MAX_IO_CHANS)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel::Channel(ChannelType type, ID id, ID columnId, ChannelShared& s)
+: shared(&s)
+, id(id)
+, type(type)
+, columnId(columnId)
+, volume(G_DEFAULT_VOL)
+, volume_i(G_DEFAULT_VOL)
+, pan(G_DEFAULT_PAN)
+, armed(false)
+, key(0)
+, hasActions(false)
+, height(G_GUI_UNIT)
+, midiLighter(g_engine.midiMapper)
+, m_mute(false)
+, m_solo(false)
+{
+       switch (type)
+       {
+       case ChannelType::SAMPLE:
+               samplePlayer.emplace(&(shared->resampler.value()));
+               sampleAdvancer.emplace();
+               sampleReactor.emplace(*this, id);
+               audioReceiver.emplace();
+               sampleActionRecorder.emplace(g_engine.actionRecorder, g_engine.sequencer);
+               break;
+
+       case ChannelType::PREVIEW:
+               samplePlayer.emplace(&(shared->resampler.value()));
+               sampleReactor.emplace(*this, id);
+               break;
+
+       case ChannelType::MIDI:
+               midiController.emplace();
+               midiSender.emplace(g_engine.kernelMidi);
+               midiActionRecorder.emplace(g_engine.actionRecorder, g_engine.sequencer);
+#ifdef WITH_VST
+               midiReceiver.emplace();
+#endif
+               break;
+
+       default:
+               break;
+       }
+
+       initCallbacks();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel::Channel(const Patch::Channel& p, ChannelShared& s, float samplerateRatio, Wave* wave)
+: shared(&s)
+, id(p.id)
+, type(p.type)
+, columnId(p.columnId)
+, volume(p.volume)
+, volume_i(G_DEFAULT_VOL)
+, pan(p.pan)
+, armed(p.armed)
+, key(p.key)
+, hasActions(p.hasActions)
+, name(p.name)
+, height(p.height)
+#ifdef WITH_VST
+, plugins(g_engine.pluginManager.hydratePlugins(p.pluginIds, g_engine.model)) // TODO move outside, as constructor parameter
+#endif
+, midiLearner(p)
+, midiLighter(g_engine.midiMapper, p)
+, m_mute(p.mute)
+, m_solo(p.solo)
+{
+       shared->readActions.store(p.readActions);
+       shared->recStatus.store(p.readActions ? ChannelStatus::PLAY : ChannelStatus::OFF);
+
+       switch (type)
+       {
+       case ChannelType::SAMPLE:
+               samplePlayer.emplace(p, samplerateRatio, &(shared->resampler.value()), wave);
+               sampleAdvancer.emplace();
+               sampleReactor.emplace(*this, id);
+               audioReceiver.emplace(p);
+               sampleActionRecorder.emplace(g_engine.actionRecorder, g_engine.sequencer);
+               break;
+
+       case ChannelType::PREVIEW:
+               samplePlayer.emplace(p, samplerateRatio, &(shared->resampler.value()), nullptr);
+               sampleReactor.emplace(*this, id);
+               break;
+
+       case ChannelType::MIDI:
+               midiController.emplace();
+               midiSender.emplace(p, g_engine.kernelMidi);
+               midiActionRecorder.emplace(g_engine.actionRecorder, g_engine.sequencer);
+#ifdef WITH_VST
+               midiReceiver.emplace();
+#endif
+               break;
+
+       default:
+               break;
+       }
+
+       initCallbacks();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel::Channel(const Channel& other)
+: midiLighter(g_engine.midiMapper)
+{
+       *this = other;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel& Channel::operator=(const Channel& other)
+{
+       if (this == &other)
+               return *this;
+
+       shared     = other.shared;
+       id         = other.id;
+       type       = other.type;
+       columnId   = other.columnId;
+       volume     = other.volume;
+       volume_i   = other.volume_i;
+       pan        = other.pan;
+       m_mute     = other.m_mute;
+       m_solo     = other.m_solo;
+       armed      = other.armed;
+       key        = other.key;
+       hasActions = other.hasActions;
+       name       = other.name;
+       height     = other.height;
+#ifdef WITH_VST
+       plugins = other.plugins;
+#endif
+
+       midiLearner    = other.midiLearner;
+       midiLighter    = other.midiLighter;
+       samplePlayer   = other.samplePlayer;
+       sampleAdvancer = other.sampleAdvancer;
+       sampleReactor  = other.sampleReactor;
+       audioReceiver  = other.audioReceiver;
+       midiController = other.midiController;
+#ifdef WITH_VST
+       midiReceiver = other.midiReceiver;
+#endif
+       midiSender           = other.midiSender;
+       sampleActionRecorder = other.sampleActionRecorder;
+       midiActionRecorder   = other.midiActionRecorder;
+
+       initCallbacks();
+
+       return *this;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Channel::operator==(const Channel& other)
+{
+       return id == other.id;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Channel::isInternal() const
+{
+       return type == ChannelType::MASTER || type == ChannelType::PREVIEW;
+}
+
+bool Channel::isMuted() const
+{
+       /* Internals can't be muted. */
+       return !isInternal() && m_mute;
+}
+
+bool Channel::isSoloed() const
+{
+       return m_solo;
+}
+
+bool Channel::canInputRec() const
+{
+       if (type != ChannelType::SAMPLE)
+               return false;
+
+       bool hasWave     = samplePlayer->hasWave();
+       bool isProtected = audioReceiver->overdubProtection;
+       bool canOverdub  = !hasWave || (hasWave && !isProtected);
+
+       return armed && canOverdub;
+}
+
+bool Channel::canActionRec() const
+{
+       return hasWave() && !samplePlayer->isAnyLoopMode();
+}
+
+bool Channel::hasWave() const
+{
+       return samplePlayer && samplePlayer->hasWave();
+}
+
+bool Channel::isPlaying() const
+{
+       ChannelStatus s = shared->playStatus.load();
+       return s == ChannelStatus::PLAY || s == ChannelStatus::ENDING;
+}
+
+bool Channel::isReadingActions() const
+{
+       ChannelStatus s = shared->recStatus.load();
+       return s == ChannelStatus::PLAY || s == ChannelStatus::ENDING;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::setMute(bool v)
+{
+       if (m_mute != v)
+               midiLighter.sendMute(v);
+       m_mute = v;
+}
+
+void Channel::setSolo(bool v)
+{
+       if (m_solo != v)
+               midiLighter.sendSolo(v);
+       m_solo = v;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::initCallbacks()
+{
+       shared->playStatus.onChange = [this](ChannelStatus status) {
+               midiLighter.sendStatus(status, g_engine.mixer.isChannelAudible(*this));
+       };
+
+       if (samplePlayer)
+       {
+               samplePlayer->onLastFrame = [this](bool natural) {
+                       sampleAdvancer->onLastFrame(*this, g_engine.sequencer.isRunning(), natural);
+               };
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::advance(const Sequencer::EventBuffer& events, Range<Frame> block, Frame quantizerStep) const
+{
+       if (shared->quantizer)
+               shared->quantizer->advance(block, quantizerStep);
+
+       for (const Sequencer::Event& e : events)
+       {
+               if (midiController)
+                       midiController->advance(*this, e);
+               if (samplePlayer)
+                       sampleAdvancer->advance(*this, e);
+               if (midiSender)
+                       midiSender->advance(*this, e);
+#ifdef WITH_VST
+               if (midiReceiver)
+                       midiReceiver->advance(*this, e);
+#endif
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::react(const EventDispatcher::EventBuffer& events)
+{
+       for (const EventDispatcher::Event& e : events)
+       {
+               if (e.channelId > 0 && e.channelId != id)
+                       continue;
+
+               react(e);
+               if (midiController)
+                       midiController->react(*this, e);
+               if (midiSender)
+                       midiSender->react(*this, e);
+               if (samplePlayer)
+                       samplePlayer->react(e);
+               if (midiActionRecorder)
+                       midiActionRecorder->react(*this, e, g_engine.recorder.canRecordActions());
+               if (sampleActionRecorder)
+                       sampleActionRecorder->react(*this, e, g_engine.conf.data.treatRecsAsLoops,
+                           g_engine.sequencer.isRunning(), g_engine.recorder.canRecordActions());
+               if (sampleReactor)
+                       sampleReactor->react(*this, e, g_engine.sequencer, g_engine.conf.data);
+#ifdef WITH_VST
+               if (midiReceiver)
+                       midiReceiver->react(*this, e);
+#endif
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::react(const EventDispatcher::Event& e)
+{
+       switch (e.type)
+       {
+       case EventDispatcher::EventType::CHANNEL_VOLUME:
+               volume = std::get<float>(e.data);
+               break;
+
+       case EventDispatcher::EventType::CHANNEL_PAN:
+               pan = std::get<float>(e.data);
+               break;
+
+       case EventDispatcher::EventType::CHANNEL_MUTE:
+               setMute(!isMuted());
+               break;
+
+       case EventDispatcher::EventType::CHANNEL_TOGGLE_ARM:
+               armed = !armed;
+               break;
+
+       case EventDispatcher::EventType::CHANNEL_SOLO:
+               setSolo(!isSoloed());
+               g_engine.mixerHandler.updateSoloCount();
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::render(mcl::AudioBuffer* out, mcl::AudioBuffer* in, bool audible) const
+{
+       if (id == Mixer::MASTER_OUT_CHANNEL_ID)
+               renderMasterOut(*out);
+       else if (id == Mixer::MASTER_IN_CHANNEL_ID)
+               renderMasterIn(*in);
+       else
+               renderChannel(*out, *in, audible);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::renderMasterOut(mcl::AudioBuffer& out) const
+{
+       shared->audioBuffer.set(out, /*gain=*/1.0f);
+#ifdef WITH_VST
+       if (plugins.size() > 0)
+               g_engine.pluginHost.processStack(shared->audioBuffer, plugins, nullptr);
+#endif
+       out.set(shared->audioBuffer, volume);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::renderMasterIn(mcl::AudioBuffer& in) const
+{
+#ifdef WITH_VST
+       if (plugins.size() > 0)
+               g_engine.pluginHost.processStack(in, plugins, nullptr);
+#else
+       (void)in;
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Channel::renderChannel(mcl::AudioBuffer& out, mcl::AudioBuffer& in, bool audible) const
+{
+       shared->audioBuffer.clear();
+
+       if (samplePlayer && isPlaying())
+       {
+               SamplePlayer::Render render;
+               while (shared->renderQueue->pop(render))
+                       ;
+               samplePlayer->render(*shared, render);
+       }
+
+       if (audioReceiver)
+               audioReceiver->render(*this, in);
+
+               /* If MidiReceiver exists, let it process the plug-in stack, as it can 
+       contain plug-ins that take MIDI events (i.e. synths). Otherwise process the
+       plug-in stack internally with no MIDI events. */
+
+#ifdef WITH_VST
+       if (midiReceiver)
+               midiReceiver->render(*this, g_engine.pluginHost);
+       else if (plugins.size() > 0)
+               g_engine.pluginHost.processStack(shared->audioBuffer, plugins, nullptr);
+#endif
+
+       if (audible)
+               out.sum(shared->audioBuffer, volume * volume_i, calcPanning_(pan));
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/channel.h b/src/core/channels/channel.h
new file mode 100644 (file)
index 0000000..020ba9e
--- /dev/null
@@ -0,0 +1,178 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_H
+#define G_CHANNEL_H
+
+#include <optional>
+#ifdef WITH_VST
+#include "deps/juce-config.h"
+#endif
+#include "core/channels/audioReceiver.h"
+#include "core/channels/midiActionRecorder.h"
+#include "core/channels/midiController.h"
+#include "core/channels/midiLearner.h"
+#include "core/channels/midiLighter.h"
+#include "core/channels/midiSender.h"
+#include "core/channels/sampleActionRecorder.h"
+#include "core/channels/sampleAdvancer.h"
+#include "core/channels/samplePlayer.h"
+#include "core/channels/sampleReactor.h"
+#include "core/const.h"
+#include "core/eventDispatcher.h"
+#include "core/midiEvent.h"
+#include "core/mixer.h"
+#include "core/patch.h"
+#include "core/queue.h"
+#include "core/resampler.h"
+#include "core/sequencer.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#ifdef WITH_VST
+#include "core/channels/midiReceiver.h"
+#endif
+
+namespace giada::m
+{
+class Plugin;
+
+struct ChannelShared final
+{
+       ChannelShared(Frame bufferSize);
+
+       mcl::AudioBuffer audioBuffer;
+#ifdef WITH_VST
+       juce::MidiBuffer     midiBuffer;
+       Queue<MidiEvent, 32> midiQueue;
+#endif
+
+       WeakAtomic<Frame>         tracker     = 0;
+       WeakAtomic<ChannelStatus> playStatus  = ChannelStatus::OFF;
+       WeakAtomic<ChannelStatus> recStatus   = ChannelStatus::OFF;
+       WeakAtomic<bool>          readActions = false;
+
+       std::optional<Quantizer> quantizer;
+
+       /* Optional render queue for sample-based channels. Used by SampleReactor
+       and SampleAdvancer to instruct SamplePlayer how to render audio. */
+
+       std::optional<Queue<SamplePlayer::Render, 2>> renderQueue = {};
+
+       /* Optional resampler for sample-based channels. Unfortunately a Resampler
+       object (based on libsamplerate) doesn't like to get copied while rendering
+       audio, so can't live inside WaveReader object (which is copied on model 
+       changes by the Swapper mechanism). Let's put it in the shared state here. */
+
+       std::optional<Resampler> resampler = {};
+};
+
+/* -------------------------------------------------------------------------- */
+
+class Channel final
+{
+public:
+       Channel(ChannelType t, ID id, ID columnId, ChannelShared&);
+       Channel(const Patch::Channel& p, ChannelShared&, float samplerateRatio, Wave* w);
+       Channel(const Channel& o);
+       Channel(Channel&& o) = default;
+
+       Channel& operator=(const Channel&);
+       Channel& operator=(Channel&&) = default;
+       bool     operator==(const Channel&);
+
+       /* advance
+       Advances internal state by processing static events (e.g. pre-recorded 
+       actions or sequencer events) in the current block. */
+
+       void advance(const Sequencer::EventBuffer&, Range<Frame>, Frame quantizerStep) const;
+
+       /* render
+       Renders audio data to I/O buffers. */
+
+       void render(mcl::AudioBuffer* out, mcl::AudioBuffer* in, bool audible) const;
+
+       /* react
+       Reacts to live events coming from the EventDispatcher (human events) and
+       updates itself accordingly. */
+
+       void react(const EventDispatcher::EventBuffer& e);
+
+       bool isPlaying() const;
+       bool isReadingActions() const;
+       bool isInternal() const;
+       bool isMuted() const;
+       bool isSoloed() const;
+       bool canInputRec() const;
+       bool canActionRec() const;
+       bool hasWave() const;
+
+       void setMute(bool);
+       void setSolo(bool);
+
+       ChannelShared* shared;
+       ID             id;
+       ChannelType    type;
+       ID             columnId;
+       float          volume;
+       float          volume_i; // Internal volume used for velocity-drives-volume mode on Sample Channels
+       float          pan;
+       bool           armed;
+       int            key;
+       bool           hasActions;
+       std::string    name;
+       Pixel          height;
+#ifdef WITH_VST
+       std::vector<Plugin*> plugins;
+#endif
+
+       MidiLearner             midiLearner;
+       MidiLighter<KernelMidi> midiLighter;
+
+       std::optional<SamplePlayer>   samplePlayer;
+       std::optional<SampleAdvancer> sampleAdvancer;
+       std::optional<SampleReactor>  sampleReactor;
+       std::optional<AudioReceiver>  audioReceiver;
+       std::optional<MidiController> midiController;
+#ifdef WITH_VST
+       std::optional<MidiReceiver> midiReceiver;
+#endif
+       std::optional<MidiSender>           midiSender;
+       std::optional<SampleActionRecorder> sampleActionRecorder;
+       std::optional<MidiActionRecorder>   midiActionRecorder;
+
+private:
+       void renderMasterOut(mcl::AudioBuffer&) const;
+       void renderMasterIn(mcl::AudioBuffer&) const;
+       void renderChannel(mcl::AudioBuffer& out, mcl::AudioBuffer& in, bool audible) const;
+
+       void initCallbacks();
+       void react(const EventDispatcher::Event&);
+
+       bool m_mute;
+       bool m_solo;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/channelManager.cpp b/src/core/channels/channelManager.cpp
new file mode 100644 (file)
index 0000000..51d42e8
--- /dev/null
@@ -0,0 +1,181 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/channels/channelManager.h"
+#include "core/channels/channel.h"
+#include "core/channels/samplePlayer.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "core/patch.h"
+#include "core/plugins/plugin.h"
+#include "core/plugins/pluginHost.h"
+#include "core/wave.h"
+#include "glue/channel.h"
+#include <cassert>
+#include <memory>
+
+namespace giada::m
+{
+ChannelManager::ChannelManager(const Conf::Data& c, model::Model& m)
+: m_conf(c)
+, m_model(m)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+ID ChannelManager::getNextId() const
+{
+       return m_channelId.getNext();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void ChannelManager::reset()
+{
+       m_channelId = IdManager();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel ChannelManager::create(ID channelId, ChannelType type, ID columnId, int bufferSize)
+{
+       Channel out = Channel(type, m_channelId.generate(channelId), columnId,
+           makeShared(type, bufferSize));
+
+       if (out.audioReceiver)
+               out.audioReceiver->overdubProtection = m_conf.overdubProtectionDefaultOn;
+
+       c::channel::setCallbacks(out); // UI callbacks
+
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel ChannelManager::create(const Channel& o, int bufferSize)
+{
+       Channel out = Channel(o);
+
+       out.id     = m_channelId.generate();
+       out.shared = &makeShared(o.type, bufferSize);
+
+       c::channel::setCallbacks(out); // UI callbacks
+
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel ChannelManager::deserializeChannel(const Patch::Channel& pch, float samplerateRatio, int bufferSize)
+{
+       m_channelId.set(pch.id);
+
+       Channel out = Channel(pch, makeShared(pch.type, bufferSize), samplerateRatio, m_model.findShared<Wave>(pch.waveId));
+       c::channel::setCallbacks(out); // UI callbacks
+
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+const Patch::Channel ChannelManager::serializeChannel(const Channel& c)
+{
+       Patch::Channel pc;
+
+#ifdef WITH_VST
+       for (const Plugin* p : c.plugins)
+               pc.pluginIds.push_back(p->id);
+#endif
+
+       pc.id                = c.id;
+       pc.type              = c.type;
+       pc.columnId          = c.columnId;
+       pc.height            = c.height;
+       pc.name              = c.name;
+       pc.key               = c.key;
+       pc.mute              = c.isMuted();
+       pc.solo              = c.isSoloed();
+       pc.volume            = c.volume;
+       pc.pan               = c.pan;
+       pc.hasActions        = c.hasActions;
+       pc.readActions       = c.shared->readActions.load();
+       pc.armed             = c.armed;
+       pc.midiIn            = c.midiLearner.enabled;
+       pc.midiInFilter      = c.midiLearner.filter;
+       pc.midiInKeyPress    = c.midiLearner.keyPress.getValue();
+       pc.midiInKeyRel      = c.midiLearner.keyRelease.getValue();
+       pc.midiInKill        = c.midiLearner.kill.getValue();
+       pc.midiInArm         = c.midiLearner.arm.getValue();
+       pc.midiInVolume      = c.midiLearner.volume.getValue();
+       pc.midiInMute        = c.midiLearner.mute.getValue();
+       pc.midiInSolo        = c.midiLearner.solo.getValue();
+       pc.midiInReadActions = c.midiLearner.readActions.getValue();
+       pc.midiInPitch       = c.midiLearner.pitch.getValue();
+       pc.midiOutL          = c.midiLighter.enabled;
+       pc.midiOutLplaying   = c.midiLighter.playing.getValue();
+       pc.midiOutLmute      = c.midiLighter.mute.getValue();
+       pc.midiOutLsolo      = c.midiLighter.solo.getValue();
+
+       if (c.type == ChannelType::SAMPLE)
+       {
+               pc.waveId            = c.samplePlayer->getWaveId();
+               pc.mode              = c.samplePlayer->mode;
+               pc.begin             = c.samplePlayer->begin;
+               pc.end               = c.samplePlayer->end;
+               pc.pitch             = c.samplePlayer->pitch;
+               pc.shift             = c.samplePlayer->shift;
+               pc.midiInVeloAsVol   = c.samplePlayer->velocityAsVol;
+               pc.inputMonitor      = c.audioReceiver->inputMonitor;
+               pc.overdubProtection = c.audioReceiver->overdubProtection;
+       }
+       else if (c.type == ChannelType::MIDI)
+       {
+               pc.midiOut     = c.midiSender->enabled;
+               pc.midiOutChan = c.midiSender->filter;
+       }
+
+       return pc;
+}
+
+/* -------------------------------------------------------------------------- */
+
+ChannelShared& ChannelManager::makeShared(ChannelType type, int bufferSize)
+{
+       std::unique_ptr<ChannelShared> shared = std::make_unique<ChannelShared>(bufferSize);
+
+       if (type == ChannelType::SAMPLE || type == ChannelType::PREVIEW)
+       {
+               shared->quantizer.emplace();
+               shared->renderQueue.emplace();
+               shared->resampler.emplace(static_cast<Resampler::Quality>(m_conf.rsmpQuality), G_MAX_IO_CHANS);
+       }
+
+       m_model.addShared(std::move(shared));
+       return m_model.backShared<ChannelShared>();
+}
+} // namespace giada::m
diff --git a/src/core/channels/channelManager.h b/src/core/channels/channelManager.h
new file mode 100644 (file)
index 0000000..29b8290
--- /dev/null
@@ -0,0 +1,86 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_MANAGER_H
+#define G_CHANNEL_MANAGER_H
+
+#include "core/channels/channel.h"
+#include "core/conf.h"
+#include "core/idManager.h"
+#include "core/patch.h"
+#include "core/types.h"
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::m
+{
+class KernelAudio;
+class ChannelManager final
+{
+public:
+       ChannelManager(const Conf::Data&, model::Model&);
+
+       /* getNextId
+       Returns the next channel ID that will be assigned to a new channel. */
+
+       ID getNextId() const;
+
+       /* reset
+    Resets internal ID generator. */
+
+       void reset();
+
+       /* create (1)
+    Creates a new channel. If channelId == 0 generates a new ID, reuse the one 
+    passed in otherwise. */
+
+       Channel create(ID channelId, ChannelType type, ID columnId, int bufferSize);
+
+       /* create (2)
+    Creates a new channel given an existing one (i.e. clone). */
+
+       Channel create(const Channel& ch, int bufferSize);
+
+       /* (de)serializeWave
+    Creates a new channel given the patch raw data and vice versa. */
+
+       Channel              deserializeChannel(const Patch::Channel& c, float samplerateRatio, int bufferSize);
+       const Patch::Channel serializeChannel(const Channel& c);
+
+private:
+       ChannelShared& makeShared(ChannelType type, int bufferSize);
+
+       IdManager m_channelId;
+
+       const Conf::Data& m_conf;
+       model::Model&     m_model;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/midiActionRecorder.cpp b/src/core/channels/midiActionRecorder.cpp
new file mode 100644 (file)
index 0000000..0b5286e
--- /dev/null
@@ -0,0 +1,55 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/channels/midiActionRecorder.h"
+#include "core/channels/channel.h"
+#include "core/conf.h"
+#include "core/eventDispatcher.h"
+#include "core/sequencer.h"
+#include "src/core/actions/action.h"
+#include "src/core/actions/actionRecorder.h"
+
+namespace giada::m
+{
+MidiActionRecorder::MidiActionRecorder(ActionRecorder& a, Sequencer& s)
+: m_actionRecorder(&a)
+, m_sequencer(&s)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiActionRecorder::react(Channel& ch, const EventDispatcher::Event& e, bool canRecordActions)
+{
+       if (e.type == EventDispatcher::EventType::MIDI && canRecordActions)
+       {
+               MidiEvent flat(std::get<Action>(e.data).event);
+               flat.setChannel(0);
+               m_actionRecorder->liveRec(ch.id, flat, m_sequencer->getCurrentFrameQuantized());
+               ch.hasActions = true;
+       }
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/midiActionRecorder.h b/src/core/channels/midiActionRecorder.h
new file mode 100644 (file)
index 0000000..62f9807
--- /dev/null
@@ -0,0 +1,51 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_MIDI_ACTION_RECORDER_H
+#define G_CHANNEL_MIDI_ACTION_RECORDER_H
+
+#include "core/eventDispatcher.h"
+
+namespace giada::m
+{
+class ActionRecorder;
+class Sequencer;
+class Channel;
+class MidiActionRecorder final
+{
+public:
+       MidiActionRecorder(ActionRecorder&, Sequencer&);
+
+       void react(Channel&, const EventDispatcher::Event&, bool canRecordActions);
+
+private:
+       ActionRecorder* m_actionRecorder;
+       Sequencer*      m_sequencer;
+};
+
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/midiController.cpp b/src/core/channels/midiController.cpp
new file mode 100644 (file)
index 0000000..3ad920d
--- /dev/null
@@ -0,0 +1,104 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiController.h"
+#include "core/channels/channel.h"
+#include "core/conf.h"
+#include <cassert>
+
+namespace giada::m
+{
+void MidiController::react(Channel& ch, const EventDispatcher::Event& e) const
+{
+       switch (e.type)
+       {
+       case EventDispatcher::EventType::KEY_PRESS:
+               ch.shared->playStatus.store(press(ch));
+               break;
+
+       case EventDispatcher::EventType::KEY_KILL:
+       case EventDispatcher::EventType::SEQUENCER_STOP:
+               ch.shared->playStatus.store(ChannelStatus::OFF);
+               break;
+
+       case EventDispatcher::EventType::SEQUENCER_REWIND:
+               ch.shared->playStatus.store(onFirstBeat(ch));
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiController::advance(const Channel& ch, const Sequencer::Event& e) const
+{
+       if (e.type == Sequencer::EventType::FIRST_BEAT)
+               ch.shared->playStatus.store(onFirstBeat(ch));
+}
+
+/* -------------------------------------------------------------------------- */
+
+ChannelStatus MidiController::onFirstBeat(const Channel& ch) const
+{
+       ChannelStatus playStatus = ch.shared->playStatus.load();
+
+       if (playStatus == ChannelStatus::ENDING)
+               playStatus = ChannelStatus::OFF;
+       else if (playStatus == ChannelStatus::WAIT)
+               playStatus = ChannelStatus::PLAY;
+
+       return playStatus;
+}
+
+/* -------------------------------------------------------------------------- */
+
+ChannelStatus MidiController::press(const Channel& ch) const
+{
+       ChannelStatus playStatus = ch.shared->playStatus.load();
+
+       switch (playStatus)
+       {
+       case ChannelStatus::PLAY:
+               playStatus = ChannelStatus::ENDING;
+               break;
+
+       case ChannelStatus::ENDING:
+       case ChannelStatus::WAIT:
+               playStatus = ChannelStatus::OFF;
+               break;
+
+       case ChannelStatus::OFF:
+               playStatus = ChannelStatus::WAIT;
+               break;
+
+       default:
+               break;
+       }
+
+       return playStatus;
+}
+} // namespace giada::m
diff --git a/src/core/channels/midiController.h b/src/core/channels/midiController.h
new file mode 100644 (file)
index 0000000..61c701e
--- /dev/null
@@ -0,0 +1,47 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_MIDI_CONTROLLER_H
+#define G_CHANNEL_MIDI_CONTROLLER_H
+
+#include "core/sequencer.h"
+
+namespace giada::m
+{
+class Channel;
+class MidiController final
+{
+public:
+       void advance(const Channel& ch, const Sequencer::Event& e) const;
+       void react(Channel& ch, const EventDispatcher::Event& e) const;
+
+private:
+       ChannelStatus onFirstBeat(const Channel& ch) const;
+       ChannelStatus press(const Channel& ch) const;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/midiLearner.cpp b/src/core/channels/midiLearner.cpp
new file mode 100644 (file)
index 0000000..3c6d92f
--- /dev/null
@@ -0,0 +1,61 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiLearner.h"
+#include "core/patch.h"
+
+namespace giada::m
+{
+MidiLearner::MidiLearner()
+: enabled(false)
+, filter(-1)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+MidiLearner::MidiLearner(const Patch::Channel& p)
+: enabled(p.midiIn)
+, filter(p.midiInFilter)
+, keyPress(p.midiInKeyPress)
+, keyRelease(p.midiInKeyRel)
+, kill(p.midiInKill)
+, arm(p.midiInArm)
+, volume(p.midiInVolume)
+, mute(p.midiInMute)
+, solo(p.midiInSolo)
+, readActions(p.midiInReadActions)
+, pitch(p.midiInPitch)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool MidiLearner::isAllowed(int c) const
+{
+       return enabled && (filter == -1 || filter == c);
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/midiLearner.h b/src/core/channels/midiLearner.h
new file mode 100644 (file)
index 0000000..4bd64a1
--- /dev/null
@@ -0,0 +1,72 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_MIDI_LEARNER_H
+#define G_CHANNEL_MIDI_LEARNER_H
+
+#include "core/midiLearnParam.h"
+#include "core/patch.h"
+
+namespace giada::m
+{
+class MidiLearner final
+{
+public:
+       MidiLearner();
+       MidiLearner(const Patch::Channel&);
+       MidiLearner(const MidiLearner&) = default;
+
+       /* isAllowed
+    Tells whether the MIDI channel 'c' is enabled to receive MIDI data. */
+
+       bool isAllowed(int c) const;
+
+       /* enabled
+    Tells whether MIDI learning is enabled for the current channel. */
+
+       bool enabled;
+
+       /* filter
+    Which MIDI channel should be filtered out when receiving MIDI messages. 
+    If -1 means 'all'. */
+
+       int filter;
+
+       /* MIDI learning fields. */
+
+       MidiLearnParam keyPress;
+       MidiLearnParam keyRelease;
+       MidiLearnParam kill;
+       MidiLearnParam arm;
+       MidiLearnParam volume;
+       MidiLearnParam mute;
+       MidiLearnParam solo;
+       MidiLearnParam readActions; // Sample Channels only
+       MidiLearnParam pitch;       // Sample Channels only
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/midiLighter.cpp b/src/core/channels/midiLighter.cpp
new file mode 100644 (file)
index 0000000..d21415f
--- /dev/null
@@ -0,0 +1,128 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/channels/midiLighter.h"
+#include "core/kernelMidi.h"
+#include "core/midiMapper.h"
+
+namespace giada::m
+{
+template <typename KernelMidiI>
+MidiLighter<KernelMidiI>::MidiLighter(MidiMapper<KernelMidiI>& m)
+: enabled(false)
+, onSend(nullptr)
+, m_midiMapper(&m)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+MidiLighter<KernelMidiI>::MidiLighter(MidiMapper<KernelMidiI>& m, const Patch::Channel& p)
+: enabled(p.midiOutL)
+, playing(p.midiOutLplaying)
+, mute(p.midiOutLmute)
+, solo(p.midiOutLsolo)
+, m_midiMapper(&m)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+void MidiLighter<KernelMidiI>::sendStatus(ChannelStatus status, bool audible)
+{
+       const MidiMap& midiMap   = m_midiMapper->currentMap;
+       const uint32_t l_playing = playing.getValue();
+
+       if (l_playing == 0x0)
+               return;
+
+       switch (status)
+       {
+       case ChannelStatus::OFF:
+               send(l_playing, midiMap.stopped);
+               break;
+
+       case ChannelStatus::WAIT:
+               send(l_playing, midiMap.waiting);
+               break;
+
+       case ChannelStatus::ENDING:
+               send(l_playing, midiMap.stopping);
+               break;
+
+       case ChannelStatus::PLAY:
+               send(l_playing, audible ? midiMap.playing : midiMap.playingInaudible);
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+void MidiLighter<KernelMidiI>::sendMute(bool isMuted)
+{
+       const MidiMap& midiMap = m_midiMapper->currentMap;
+       const uint32_t l_mute  = mute.getValue();
+
+       if (l_mute != 0x0)
+               send(l_mute, isMuted ? midiMap.muteOn : midiMap.muteOff);
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+void MidiLighter<KernelMidiI>::sendSolo(bool isSoloed)
+{
+       const MidiMap& midiMap = m_midiMapper->currentMap;
+       const uint32_t l_solo  = solo.getValue();
+
+       if (l_solo != 0x0)
+               send(l_solo, isSoloed ? midiMap.soloOn : midiMap.soloOff);
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+void MidiLighter<KernelMidiI>::send(uint32_t learnt, const MidiMap::Message& msg)
+{
+       assert(onSend != nullptr);
+
+       m_midiMapper->sendMidiLightning(learnt, msg);
+       onSend();
+}
+
+/* -------------------------------------------------------------------------- */
+
+template class MidiLighter<KernelMidi>;
+#ifdef WITH_TESTS
+template class MidiLighter<KernelMidiMock>;
+#endif
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/midiLighter.h b/src/core/channels/midiLighter.h
new file mode 100644 (file)
index 0000000..8d540da
--- /dev/null
@@ -0,0 +1,77 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_MIDI_LIGHTER_H
+#define G_CHANNEL_MIDI_LIGHTER_H
+
+#include "core/eventDispatcher.h"
+#include "core/midiLearnParam.h"
+#include "core/midiMapper.h"
+#include "core/patch.h"
+
+namespace giada::m
+{
+template <typename KernelMidiI>
+class MidiLighter final
+{
+public:
+       MidiLighter(MidiMapper<KernelMidiI>&);
+       MidiLighter(MidiMapper<KernelMidiI>&, const Patch::Channel&);
+       MidiLighter(const MidiLighter& o) = default;
+
+       void sendStatus(ChannelStatus, bool audible);
+       void sendMute(bool isMuted);
+       void sendSolo(bool isSoloed);
+
+       /* enabled
+    Tells whether MIDI lighting is enabled or not. */
+
+       bool enabled;
+
+       /* MIDI learning fields for MIDI lighting. */
+
+       MidiLearnParam playing;
+       MidiLearnParam mute;
+       MidiLearnParam solo;
+
+       /* onSend
+       Callback fired when a MIDI signal has been sent. */
+
+       std::function<void()> onSend;
+
+private:
+       void send(uint32_t learnt, const MidiMap::Message&);
+
+       MidiMapper<KernelMidiI>* m_midiMapper;
+};
+
+extern template class MidiLighter<KernelMidi>;
+#ifdef WITH_TESTS
+extern template class MidiLighter<KernelMidiMock>;
+#endif
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/midiReceiver.cpp b/src/core/channels/midiReceiver.cpp
new file mode 100644 (file)
index 0000000..599f2ca
--- /dev/null
@@ -0,0 +1,106 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "midiReceiver.h"
+#include "core/channels/channel.h"
+#include "core/eventDispatcher.h"
+#include "core/mixer.h"
+#include "core/plugins/pluginHost.h"
+
+namespace giada::m
+{
+void MidiReceiver::react(const Channel& ch, const EventDispatcher::Event& e) const
+{
+       switch (e.type)
+       {
+       case EventDispatcher::EventType::MIDI:
+               parseMidi(ch, std::get<Action>(e.data).event);
+               break;
+
+       case EventDispatcher::EventType::KEY_KILL:
+       case EventDispatcher::EventType::SEQUENCER_STOP:
+       case EventDispatcher::EventType::SEQUENCER_REWIND:
+               sendToPlugins(ch, MidiEvent(G_MIDI_ALL_NOTES_OFF), 0);
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiReceiver::advance(const Channel& ch, const Sequencer::Event& e) const
+{
+       if (e.type == Sequencer::EventType::ACTIONS && ch.isPlaying())
+               for (const Action& action : *e.actions)
+                       if (action.channelId == ch.id)
+                               sendToPlugins(ch, action.event, e.delta);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiReceiver::render(const Channel& ch, PluginHost& pluginHost) const
+{
+       ch.shared->midiBuffer.clear();
+
+       MidiEvent e;
+       while (ch.shared->midiQueue.pop(e))
+       {
+               juce::MidiMessage message = juce::MidiMessage(
+                   e.getStatus(),
+                   e.getNote(),
+                   e.getVelocity());
+               ch.shared->midiBuffer.addEvent(message, e.getDelta());
+       }
+
+       pluginHost.processStack(ch.shared->audioBuffer, ch.plugins, &ch.shared->midiBuffer);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiReceiver::sendToPlugins(const Channel& ch, const MidiEvent& e, Frame localFrame) const
+{
+       ch.shared->midiQueue.push(MidiEvent(e.getRaw(), localFrame));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiReceiver::parseMidi(const Channel& ch, const MidiEvent& e) const
+{
+       /* Now all messages are turned into Channel-0 messages. Giada doesn't care 
+       about holding MIDI channel information. Moreover, having all internal 
+       messages on channel 0 is way easier. Then send it to plug-ins. */
+
+       MidiEvent flat(e);
+       flat.setChannel(0);
+       sendToPlugins(ch, flat, /*delta=*/0);
+}
+} // namespace giada::m
+
+#endif // WITH_VST
diff --git a/src/core/channels/midiReceiver.h b/src/core/channels/midiReceiver.h
new file mode 100644 (file)
index 0000000..4f8fdb5
--- /dev/null
@@ -0,0 +1,53 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_MIDI_RECEIVER_H
+#define G_CHANNEL_MIDI_RECEIVER_H
+
+#ifdef WITH_VST
+
+#include "core/sequencer.h"
+
+namespace giada::m
+{
+class PluginHost;
+class Channel;
+class MidiReceiver final
+{
+public:
+       void react(const Channel& ch, const EventDispatcher::Event& e) const;
+       void advance(const Channel& ch, const Sequencer::Event& e) const;
+       void render(const Channel& ch, PluginHost& plugiHost) const;
+
+private:
+       void sendToPlugins(const Channel& ch, const MidiEvent& e, Frame localFrame) const;
+       void parseMidi(const Channel& ch, const MidiEvent& e) const;
+};
+} // namespace giada::m
+
+#endif // WITH_VST
+
+#endif
diff --git a/src/core/channels/midiSender.cpp b/src/core/channels/midiSender.cpp
new file mode 100644 (file)
index 0000000..6eee0ba
--- /dev/null
@@ -0,0 +1,92 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/channels/midiSender.h"
+#include "core/channels/channel.h"
+#include "core/kernelMidi.h"
+#include "core/mixer.h"
+
+namespace giada::m
+{
+MidiSender::MidiSender(KernelMidi& k)
+: kernelMidi(&k)
+, enabled(false)
+, filter(0)
+, onSend(nullptr)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+MidiSender::MidiSender(const Patch::Channel& p, KernelMidi& k)
+: kernelMidi(&k)
+, enabled(p.midiOut)
+, filter(p.midiOutChan)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiSender::react(const Channel& ch, const EventDispatcher::Event& e)
+{
+       if (!ch.isPlaying() || !enabled || ch.isMuted())
+               return;
+
+       if (e.type == EventDispatcher::EventType::KEY_KILL ||
+           e.type == EventDispatcher::EventType::SEQUENCER_STOP)
+               send(MidiEvent(G_MIDI_ALL_NOTES_OFF));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiSender::advance(const Channel& ch, const Sequencer::Event& e) const
+{
+       if (!ch.isPlaying() || !enabled || ch.isMuted())
+               return;
+       if (e.type == Sequencer::EventType::ACTIONS)
+               parseActions(ch, *e.actions);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiSender::send(MidiEvent e) const
+{
+       assert(onSend != nullptr);
+
+       e.setChannel(filter);
+       kernelMidi->send(e.getRaw());
+       onSend();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiSender::parseActions(const Channel& ch, const std::vector<Action>& as) const
+{
+       for (const Action& a : as)
+               if (a.channelId == ch.id)
+                       send(a.event);
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/midiSender.h b/src/core/channels/midiSender.h
new file mode 100644 (file)
index 0000000..140f291
--- /dev/null
@@ -0,0 +1,70 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_MIDI_SENDER_H
+#define G_CHANNEL_MIDI_SENDER_H
+
+#include "core/patch.h"
+#include "core/sequencer.h"
+
+namespace giada::m
+{
+class KernelMidi;
+class Channel;
+class MidiSender final
+{
+public:
+       MidiSender(KernelMidi&);
+       MidiSender(const Patch::Channel& p, KernelMidi&);
+       MidiSender(const MidiSender& o) = default;
+
+       void react(const Channel& ch, const EventDispatcher::Event& e);
+       void advance(const Channel& ch, const Sequencer::Event& e) const;
+
+       KernelMidi* kernelMidi;
+
+       /* enabled
+    Tells whether MIDI output is enabled or not. */
+
+       bool enabled;
+
+       /* filter
+    Which MIDI channel data should be sent to. */
+
+       int filter;
+
+       /* onSend
+       Callback fired when a MIDI signal has been sent. */
+
+       std::function<void()> onSend;
+
+private:
+       void send(MidiEvent e) const;
+       void parseActions(const Channel& ch, const std::vector<Action>& as) const;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/sampleActionRecorder.cpp b/src/core/channels/sampleActionRecorder.cpp
new file mode 100644 (file)
index 0000000..d03c752
--- /dev/null
@@ -0,0 +1,172 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/channels/sampleActionRecorder.h"
+#include "core/channels/channel.h"
+#include "core/eventDispatcher.h"
+#include "src/core/actions/action.h"
+#include "src/core/actions/actionRecorder.h"
+#include <cassert>
+
+namespace giada::m
+{
+SampleActionRecorder::SampleActionRecorder(ActionRecorder& a, Sequencer& s)
+: m_actionRecorder(&a)
+, m_sequencer(&s)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleActionRecorder::react(Channel& ch, const EventDispatcher::Event& e, bool treatRecsAsLoops,
+    bool seqIsRunning, bool canRecordActions) const
+{
+       if (!ch.hasWave())
+               return;
+
+       canRecordActions = canRecordActions && !ch.samplePlayer->isAnyLoopMode();
+
+       switch (e.type)
+       {
+       case EventDispatcher::EventType::KEY_PRESS:
+               if (canRecordActions)
+                       onKeyPress(ch);
+               break;
+
+       case EventDispatcher::EventType::KEY_RELEASE:
+               /* Record a stop event only if channel is SINGLE_PRESS. For any other 
+               mode the key release event is meaningless. */
+               if (canRecordActions && ch.samplePlayer->mode == SamplePlayerMode::SINGLE_PRESS)
+                       record(ch, MidiEvent::NOTE_OFF);
+               break;
+
+       case EventDispatcher::EventType::KEY_KILL:
+               if (canRecordActions)
+                       record(ch, MidiEvent::NOTE_KILL);
+               break;
+
+       case EventDispatcher::EventType::CHANNEL_TOGGLE_READ_ACTIONS:
+               if (ch.hasActions)
+                       toggleReadActions(ch, treatRecsAsLoops, seqIsRunning);
+               break;
+
+       case EventDispatcher::EventType::CHANNEL_KILL_READ_ACTIONS:
+               /* Killing Read Actions, i.e. shift + click on 'R' button is meaningful 
+               only when the conf::treatRecsAsLoops is true. */
+               if (treatRecsAsLoops)
+                       killReadActions(ch);
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleActionRecorder::record(Channel& ch, int note) const
+{
+       m_actionRecorder->liveRec(ch.id, MidiEvent(note, 0, 0), m_sequencer->getCurrentFrameQuantized());
+       ch.hasActions = true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleActionRecorder::onKeyPress(Channel& ch) const
+{
+       record(ch, MidiEvent::NOTE_ON);
+
+       /* Skip reading actions when recording on ChannelMode::SINGLE_PRESS to 
+       prevent existing actions to interfere with the keypress/keyrel combo. */
+
+       if (ch.samplePlayer->mode == SamplePlayerMode::SINGLE_PRESS)
+               ch.shared->readActions.store(false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleActionRecorder::startReadActions(Channel& ch, bool treatRecsAsLoops) const
+{
+       if (treatRecsAsLoops)
+               ch.shared->recStatus.store(ChannelStatus::WAIT);
+       else
+       {
+               ch.shared->recStatus.store(ChannelStatus::PLAY);
+               ch.shared->readActions.store(true);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleActionRecorder::stopReadActions(Channel& ch, ChannelStatus curRecStatus,
+    bool treatRecsAsLoops, bool seqIsRunning) const
+{
+       /* First of all, if the sequencer is not running or treatRecsAsLoops is off, 
+       just stop and disable everything. Otherwise make sure a channel with actions
+       behave like a dynamic one. */
+
+       if (!seqIsRunning || !treatRecsAsLoops)
+       {
+               ch.shared->recStatus.store(ChannelStatus::OFF);
+               ch.shared->readActions.store(false);
+       }
+       else if (curRecStatus == ChannelStatus::WAIT)
+               ch.shared->recStatus.store(ChannelStatus::OFF);
+       else if (curRecStatus == ChannelStatus::ENDING)
+               ch.shared->recStatus.store(ChannelStatus::PLAY);
+       else
+               ch.shared->recStatus.store(ChannelStatus::ENDING);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleActionRecorder::toggleReadActions(Channel& ch, bool treatRecsAsLoops, bool seqIsRunning) const
+{
+       /* When you start reading actions while conf::treatRecsAsLoops is true, the
+       value ch.shared->readActions actually is not set to true immediately, because
+       the channel is in wait mode (REC_WAITING). readActions will become true on
+       the next first beat. So a 'stop rec' command should occur also when
+       readActions is false but the channel is in wait mode; this check will
+       handle the case of when you press 'R', the channel goes into REC_WAITING and
+       then you press 'R' again to undo the status. */
+
+       const bool          readActions = ch.shared->readActions.load();
+       const ChannelStatus recStatus   = ch.shared->recStatus.load();
+
+       if (readActions || (!readActions && recStatus == ChannelStatus::WAIT))
+               stopReadActions(ch, recStatus, treatRecsAsLoops, seqIsRunning);
+       else
+               startReadActions(ch, treatRecsAsLoops);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleActionRecorder::killReadActions(Channel& ch) const
+{
+       ch.shared->recStatus.store(ChannelStatus::OFF);
+       ch.shared->readActions.store(false);
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/sampleActionRecorder.h b/src/core/channels/sampleActionRecorder.h
new file mode 100644 (file)
index 0000000..a780c02
--- /dev/null
@@ -0,0 +1,59 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_SAMPLE_ACTION_RECORDER_H
+#define G_CHANNEL_SAMPLE_ACTION_RECORDER_H
+
+#include "core/eventDispatcher.h"
+
+namespace giada::m
+{
+class ActionRecorder;
+class Sequencer;
+class Channel;
+class SampleActionRecorder final
+{
+public:
+       SampleActionRecorder(ActionRecorder&, Sequencer&);
+
+       void react(Channel&, const EventDispatcher::Event&, bool treatRecsAsLoops,
+           bool seqIsRunning, bool canRecordActions) const;
+
+private:
+       void record(Channel&, int note) const;
+       void onKeyPress(Channel&) const;
+       void startReadActions(Channel&, bool treatRecsAsLoops) const;
+       void stopReadActions(Channel&, ChannelStatus, bool treatRecsAsLoops, bool seqIsRunning) const;
+       void toggleReadActions(Channel&, bool treatRecsAsLoops, bool seqIsRunning) const;
+       void killReadActions(Channel& ch) const;
+
+       ActionRecorder* m_actionRecorder;
+       Sequencer*      m_sequencer;
+};
+
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/sampleAdvancer.cpp b/src/core/channels/sampleAdvancer.cpp
new file mode 100644 (file)
index 0000000..9f0391a
--- /dev/null
@@ -0,0 +1,229 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/channels/sampleAdvancer.h"
+#include "core/channels/channel.h"
+#include <cassert>
+
+namespace giada::m
+{
+void SampleAdvancer::onLastFrame(const Channel& ch, bool seqIsRunning, bool natural) const
+{
+       const SamplePlayerMode mode   = ch.samplePlayer->mode;
+       const bool             isLoop = ch.samplePlayer->isAnyLoopMode();
+
+       switch (ch.shared->playStatus.load())
+       {
+       case ChannelStatus::PLAY:
+               /* Stop LOOP_* when the sequencer is off, or SINGLE_* except for
+               SINGLE_ENDLESS, which runs forever unless it's in ENDING mode. 
+               Other loop once modes are put in wait mode. */
+               if ((mode == SamplePlayerMode::SINGLE_BASIC ||
+                       mode == SamplePlayerMode::SINGLE_BASIC_PAUSE ||
+                       mode == SamplePlayerMode::SINGLE_PRESS ||
+                       mode == SamplePlayerMode::SINGLE_RETRIG) ||
+                   (isLoop && !seqIsRunning) || !natural)
+                       ch.shared->playStatus.store(ChannelStatus::OFF);
+               else if (mode == SamplePlayerMode::LOOP_ONCE || mode == SamplePlayerMode::LOOP_ONCE_BAR)
+                       ch.shared->playStatus.store(ChannelStatus::WAIT);
+               break;
+
+       case ChannelStatus::ENDING:
+               ch.shared->playStatus.store(ChannelStatus::OFF);
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleAdvancer::advance(const Channel& ch, const Sequencer::Event& e) const
+{
+       switch (e.type)
+       {
+       case Sequencer::EventType::FIRST_BEAT:
+               onFirstBeat(ch, e.delta);
+               break;
+
+       case Sequencer::EventType::BAR:
+               onBar(ch, e.delta);
+               break;
+
+       case Sequencer::EventType::REWIND:
+               if (ch.samplePlayer->isAnyLoopMode())
+                       rewind(ch, e.delta);
+               break;
+
+       case Sequencer::EventType::ACTIONS:
+               if (ch.shared->readActions.load() == true)
+                       parseActions(ch, *e.actions, e.delta);
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleAdvancer::rewind(const Channel& ch, Frame localFrame) const
+{
+       ch.shared->renderQueue->push({SamplePlayer::Render::Mode::REWIND, localFrame});
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleAdvancer::stop(const Channel& ch, Frame localFrame) const
+{
+       ch.shared->renderQueue->push({SamplePlayer::Render::Mode::STOP, localFrame});
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleAdvancer::play(const Channel& ch, Frame localFrame) const
+{
+       ch.shared->playStatus.store(ChannelStatus::PLAY);
+       ch.shared->renderQueue->push({SamplePlayer::Render::Mode::NORMAL, localFrame});
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleAdvancer::onFirstBeat(const Channel& ch, Frame localFrame) const
+{
+       G_DEBUG("onFirstBeat ch=" << ch.id << ", localFrame=" << localFrame);
+
+       const ChannelStatus playStatus = ch.shared->playStatus.load();
+       const ChannelStatus recStatus  = ch.shared->recStatus.load();
+       const bool          isLoop     = ch.samplePlayer->isAnyLoopMode();
+
+       switch (playStatus)
+       {
+       case ChannelStatus::PLAY:
+               if (isLoop)
+                       rewind(ch, localFrame);
+               break;
+
+       case ChannelStatus::WAIT:
+               play(ch, localFrame);
+               break;
+
+       case ChannelStatus::ENDING:
+               if (isLoop)
+                       stop(ch, localFrame);
+               break;
+
+       default:
+               break;
+       }
+
+       switch (recStatus)
+       {
+       case ChannelStatus::WAIT:
+               ch.shared->recStatus.store(ChannelStatus::PLAY);
+               ch.shared->readActions.store(true);
+               break;
+
+       case ChannelStatus::ENDING:
+               ch.shared->recStatus.store(ChannelStatus::OFF);
+               ch.shared->readActions.store(false);
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleAdvancer::onBar(const Channel& ch, Frame localFrame) const
+{
+       G_DEBUG("onBar ch=" << ch.id << ", localFrame=" << localFrame);
+
+       const ChannelStatus    playStatus = ch.shared->playStatus.load();
+       const SamplePlayerMode mode       = ch.samplePlayer->mode;
+
+       if (playStatus == ChannelStatus::PLAY && (mode == SamplePlayerMode::LOOP_REPEAT ||
+                                                    mode == SamplePlayerMode::LOOP_ONCE_BAR))
+               rewind(ch, localFrame);
+       else if (playStatus == ChannelStatus::WAIT && mode == SamplePlayerMode::LOOP_ONCE_BAR)
+               play(ch, localFrame);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleAdvancer::onNoteOn(const Channel& ch, Frame localFrame) const
+{
+       switch (ch.shared->playStatus.load())
+       {
+       case ChannelStatus::OFF:
+               play(ch, localFrame);
+               break;
+
+       case ChannelStatus::PLAY:
+               if (ch.samplePlayer->mode == SamplePlayerMode::SINGLE_RETRIG)
+                       rewind(ch, localFrame);
+               else
+                       stop(ch, localFrame);
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleAdvancer::parseActions(const Channel& ch,
+    const std::vector<Action>& as, Frame localFrame) const
+{
+       if (ch.samplePlayer->isAnyLoopMode() || !ch.isReadingActions())
+               return;
+
+       for (const Action& a : as)
+       {
+               if (a.channelId != ch.id)
+                       continue;
+
+               switch (a.event.getStatus())
+               {
+               case MidiEvent::NOTE_ON:
+                       onNoteOn(ch, localFrame);
+                       break;
+
+               case MidiEvent::NOTE_OFF:
+               case MidiEvent::NOTE_KILL:
+                       if (ch.shared->playStatus.load() == ChannelStatus::PLAY)
+                               stop(ch, localFrame);
+                       break;
+
+               default:
+                       break;
+               }
+       }
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/sampleAdvancer.h b/src/core/channels/sampleAdvancer.h
new file mode 100644 (file)
index 0000000..e3246c0
--- /dev/null
@@ -0,0 +1,52 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_SAMPLE_ADVANCER_H
+#define G_CHANNEL_SAMPLE_ADVANCER_H
+
+#include "core/sequencer.h"
+
+namespace giada::m
+{
+class Channel;
+class SampleAdvancer final
+{
+public:
+       void onLastFrame(const Channel& ch, bool seqIsRunning, bool natural) const;
+       void advance(const Channel& ch, const Sequencer::Event& e) const;
+
+private:
+       void rewind(const Channel& ch, Frame localFrame) const;
+       void stop(const Channel& ch, Frame localFrame) const;
+       void play(const Channel& ch, Frame localFrame) const;
+       void onFirstBeat(const Channel& ch, Frame localFrame) const;
+       void onBar(const Channel& ch, Frame localFrame) const;
+       void onNoteOn(const Channel& ch, Frame localFrame) const;
+       void parseActions(const Channel& ch, const std::vector<Action>& as, Frame localFrame) const;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/samplePlayer.cpp b/src/core/channels/samplePlayer.cpp
new file mode 100644 (file)
index 0000000..bf87e55
--- /dev/null
@@ -0,0 +1,246 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "samplePlayer.h"
+#include "core/channels/channel.h"
+#include "core/wave.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include <algorithm>
+#include <cassert>
+
+using namespace mcl;
+
+namespace giada::m
+{
+SamplePlayer::SamplePlayer(Resampler* r)
+: pitch(G_DEFAULT_PITCH)
+, mode(SamplePlayerMode::SINGLE_BASIC)
+, shift(0)
+, begin(0)
+, end(0)
+, velocityAsVol(false)
+, waveReader(r)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+SamplePlayer::SamplePlayer(const Patch::Channel& p, float samplerateRatio, Resampler* r, Wave* w)
+: pitch(p.pitch)
+, mode(p.mode)
+, shift(p.shift)
+, begin(p.begin)
+, end(p.end)
+, velocityAsVol(p.midiInVeloAsVol)
+, waveReader(r)
+, onLastFrame(nullptr)
+{
+       setWave(w, samplerateRatio);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool SamplePlayer::hasWave() const { return waveReader.wave != nullptr; }
+bool SamplePlayer::hasLogicalWave() const { return hasWave() && waveReader.wave->isLogical(); }
+bool SamplePlayer::hasEditedWave() const { return hasWave() && waveReader.wave->isEdited(); }
+
+/* -------------------------------------------------------------------------- */
+
+bool SamplePlayer::isAnyLoopMode() const
+{
+       return mode == SamplePlayerMode::LOOP_BASIC ||
+              mode == SamplePlayerMode::LOOP_ONCE ||
+              mode == SamplePlayerMode::LOOP_REPEAT ||
+              mode == SamplePlayerMode::LOOP_ONCE_BAR;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Wave* SamplePlayer::getWave() const
+{
+       return waveReader.wave;
+}
+
+ID SamplePlayer::getWaveId() const
+{
+       if (hasWave())
+               return waveReader.wave->id;
+       return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame SamplePlayer::getWaveSize() const
+{
+       return hasWave() ? waveReader.wave->getBuffer().countFrames() : 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SamplePlayer::react(const EventDispatcher::Event& e)
+{
+       if (e.type == EventDispatcher::EventType::CHANNEL_PITCH)
+               pitch = std::get<float>(e.data);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SamplePlayer::render(ChannelShared& shared, Render renderInfo) const
+{
+       if (waveReader.wave == nullptr)
+               return;
+
+       AudioBuffer&        buf     = shared.audioBuffer;
+       Frame               tracker = std::clamp(shared.tracker.load(), begin, end); /* Make sure tracker stays within begin-end range. */
+       const ChannelStatus status  = shared.playStatus.load();
+
+       if (renderInfo.mode == Render::Mode::NORMAL)
+       {
+               tracker = render(buf, tracker, renderInfo.offset, status);
+       }
+       else
+       {
+               /* Both modes: 1st = [abcdefghijklmnopq] 
+               No need for fancy render() here. You don't want the chance to trigger 
+               onLastFrame() at this point which would invalidate the rewind (a listener
+               might stop the rendering): fillBuffer() is just enough. Just notify 
+               waveReader this is the last read before rewind. */
+
+               tracker = fillBuffer(buf, tracker, 0).used;
+               waveReader.last();
+
+               /* Mode::REWIND: 2nd = [abcdefghi|abcdfefg]
+                  Mode::STOP:   2nd = [abcdefghi|--------] */
+
+               if (renderInfo.mode == Render::Mode::REWIND)
+                       tracker = render(buf, begin, renderInfo.offset, status);
+               else
+                       tracker = stop(buf, renderInfo.offset);
+       }
+
+       shared.tracker.store(tracker);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame SamplePlayer::render(AudioBuffer& buf, Frame tracker, Frame offset, ChannelStatus status) const
+{
+       /* First pass rendering. */
+
+       WaveReader::Result res = fillBuffer(buf, tracker, offset);
+       tracker += res.used;
+
+       /* Second pass rendering: if tracker has looped, special care is needed. If 
+       the     channel is in loop mode, fill the second part of the buffer with data
+       coming from the sample's head, starting at 'res.generated' offset. */
+
+       if (tracker >= end)
+       {
+               assert(onLastFrame != nullptr);
+
+               tracker = begin;
+               waveReader.last();
+               onLastFrame(/*natural=*/true);
+
+               if (shouldLoop(status) && res.generated < buf.countFrames())
+                       tracker += fillBuffer(buf, tracker, res.generated).used;
+       }
+
+       return tracker;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame SamplePlayer::stop(AudioBuffer& buf, Frame offset) const
+{
+       assert(onLastFrame != nullptr);
+
+       onLastFrame(/*natural=*/false);
+
+       if (offset != 0)
+               buf.clear(offset);
+
+       return begin;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SamplePlayer::loadWave(ChannelShared& shared, Wave* w)
+{
+       waveReader.wave = w;
+
+       shared.tracker.store(0);
+       shared.playStatus.store(w != nullptr ? ChannelStatus::OFF : ChannelStatus::EMPTY);
+       shift = 0;
+       begin = 0;
+       end   = w != nullptr ? w->getBuffer().countFrames() - 1 : 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SamplePlayer::setWave(Wave* w, float samplerateRatio)
+{
+       if (w == nullptr)
+       {
+               waveReader.wave = nullptr;
+               return;
+       }
+
+       waveReader.wave = w;
+
+       if (samplerateRatio != 1.0f)
+       {
+               begin *= samplerateRatio;
+               end *= samplerateRatio;
+               shift *= samplerateRatio;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SamplePlayer::kickIn(ChannelShared& shared, Frame f)
+{
+       shared.tracker.store(f);
+       shared.playStatus.store(ChannelStatus::PLAY);
+}
+
+/* -------------------------------------------------------------------------- */
+
+WaveReader::Result SamplePlayer::fillBuffer(AudioBuffer& buf, Frame start, Frame offset) const
+{
+       return waveReader.fill(buf, start, end, offset, pitch);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool SamplePlayer::shouldLoop(ChannelStatus status) const
+{
+       return (mode == SamplePlayerMode::LOOP_BASIC ||
+                  mode == SamplePlayerMode::LOOP_REPEAT ||
+                  mode == SamplePlayerMode::SINGLE_ENDLESS) &&
+              status == ChannelStatus::PLAY; // Don't loop if ENDING
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/samplePlayer.h b/src/core/channels/samplePlayer.h
new file mode 100644 (file)
index 0000000..f6256ab
--- /dev/null
@@ -0,0 +1,134 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_SAMPLE_PLAYER_H
+#define G_CHANNEL_SAMPLE_PLAYER_H
+
+#include "core/channels/waveReader.h"
+#include "core/const.h"
+#include "core/eventDispatcher.h"
+#include "core/patch.h"
+#include "core/sequencer.h"
+#include "core/types.h"
+#include <functional>
+
+namespace giada::m
+{
+struct ChannelShared;
+class Channel;
+class SamplePlayer final
+{
+public:
+       /* Render
+       Determines how the render() function should behave. 
+       Mode::NORMAL - normal rendering, starting at offset 'offset';
+       Mode::REWIND - two-step rendering, used when the sample must rewind at some
+               point ('offset') in the audio buffer;
+       Mode::STOP - abort rendering. The audio buffer is silenced starting at
+       'offset'. Also triggers onLastFrame(). */
+
+       struct Render
+       {
+               enum class Mode
+               {
+                       NORMAL,
+                       REWIND,
+                       STOP
+               };
+
+               Mode  mode   = Mode::NORMAL;
+               Frame offset = 0;
+       };
+
+       SamplePlayer(Resampler* r);
+       SamplePlayer(const Patch::Channel& p, float samplerateRatio, Resampler* r, Wave* w);
+
+       bool  hasWave() const;
+       bool  hasLogicalWave() const;
+       bool  hasEditedWave() const;
+       bool  isAnyLoopMode() const;
+       ID    getWaveId() const;
+       Frame getWaveSize() const;
+       Wave* getWave() const;
+       void  render(ChannelShared&, Render) const;
+
+       void react(const EventDispatcher::Event& e);
+
+       /* loadWave
+       Loads Wave and sets it up (name, markers, ...). Also updates Channel's shared
+       state accordingly. */
+
+       void loadWave(ChannelShared&, Wave*);
+
+       /* setWave
+       Just sets the pointer to a Wave object. Used during de-serialization. The
+       ratio is used to adjust begin/end points in case of patch vs. conf sample
+       rate mismatch. If nullptr, set the wave to invalid. */
+
+       void setWave(Wave* w, float samplerateRatio);
+
+       /* kickIn
+       Starts the player right away at frame 'f'. Used when launching a loop after
+       being live recorded. */
+
+       void kickIn(ChannelShared&, Frame f);
+
+       float            pitch;
+       SamplePlayerMode mode;
+       Frame            shift;
+       Frame            begin;
+       Frame            end;
+       bool             velocityAsVol; // Velocity drives volume
+       WaveReader       waveReader;
+
+       /* onLastFrame
+       Callback fired when the last frame has been reached. 'natural' == true
+       if the rendering has ended because the end of the sample has ben reached. 
+       'natural' == false if the rendering has been manually interrupted (by
+       a Render::Mode::STOP type). */
+
+       std::function<void(bool natural)> onLastFrame;
+
+private:
+       /* render
+       Renders audio into the buffer. Reads audio data from 'tracker' and copies it
+       into the audio buffer at position 'offset'. May fire 'onLastFrame' callback
+       if the sample end is reached. */
+
+       Frame render(mcl::AudioBuffer&, Frame tracker, Frame offset, ChannelStatus) const;
+
+       /* stop
+       Silences the last part of the audio buffer, starting at 'offset'. Used to
+       terminate rendering. It also fire the 'onLastFrame' callback. */
+
+       Frame stop(mcl::AudioBuffer&, Frame offset) const;
+
+       WaveReader::Result fillBuffer(mcl::AudioBuffer&, Frame start, Frame offset) const;
+       bool               shouldLoop(ChannelStatus) const;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/sampleReactor.cpp b/src/core/channels/sampleReactor.cpp
new file mode 100644 (file)
index 0000000..b009d7b
--- /dev/null
@@ -0,0 +1,245 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/channels/sampleReactor.h"
+#include "core/channels/channel.h"
+#include "core/conf.h"
+#include "core/model/model.h"
+#include "utils/math.h"
+#include <cassert>
+
+namespace giada::m
+{
+namespace
+{
+constexpr int Q_ACTION_PLAY   = 0;
+constexpr int Q_ACTION_REWIND = 10000; // Avoid clash with Q_ACTION_PLAY + channelId
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+SampleReactor::SampleReactor(Channel& ch, ID channelId)
+{
+       ch.shared->quantizer->schedule(Q_ACTION_PLAY + channelId, [this, shared = ch.shared](Frame delta) {
+               play(*shared, delta);
+       });
+
+       ch.shared->quantizer->schedule(Q_ACTION_REWIND + channelId, [this, shared = ch.shared](Frame delta) {
+               ChannelStatus status = shared->playStatus.load();
+               if (status == ChannelStatus::OFF)
+                       play(*shared, delta);
+               else if (status == ChannelStatus::PLAY || status == ChannelStatus::ENDING)
+                       rewind(*shared, delta);
+       });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleReactor::react(Channel& ch, const EventDispatcher::Event& e,
+    Sequencer& sequencer, const Conf::Data& conf) const
+{
+       if (!ch.hasWave())
+               return;
+
+       switch (e.type)
+       {
+       case EventDispatcher::EventType::KEY_PRESS:
+               press(ch, sequencer, std::get<int>(e.data));
+               break;
+
+       case EventDispatcher::EventType::KEY_RELEASE:
+               release(ch);
+               break;
+
+       case EventDispatcher::EventType::KEY_KILL:
+               if (ch.shared->playStatus.load() == ChannelStatus::PLAY)
+                       stop(*ch.shared);
+               break;
+
+       case EventDispatcher::EventType::SEQUENCER_STOP:
+               onStopBySeq(ch, conf.chansStopOnSeqHalt);
+               break;
+
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleReactor::rewind(ChannelShared& shared, Frame localFrame) const
+{
+       shared.renderQueue->push({SamplePlayer::Render::Mode::REWIND, localFrame});
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleReactor::play(ChannelShared& shared, Frame localFrame) const
+{
+       shared.playStatus.store(ChannelStatus::PLAY);
+       shared.renderQueue->push({SamplePlayer::Render::Mode::NORMAL, localFrame});
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleReactor::stop(ChannelShared& shared) const
+{
+       shared.renderQueue->push({SamplePlayer::Render::Mode::STOP, 0});
+}
+
+/* -------------------------------------------------------------------------- */
+
+ChannelStatus SampleReactor::pressWhileOff(Channel& ch, Sequencer& sequencer,
+    int velocity, bool isLoop) const
+{
+       if (isLoop)
+               return ChannelStatus::WAIT;
+
+       if (ch.samplePlayer->velocityAsVol)
+               ch.volume_i = u::math::map(velocity, G_MAX_VELOCITY, G_MAX_VOLUME);
+
+       if (sequencer.canQuantize())
+       {
+               ch.shared->quantizer->trigger(Q_ACTION_PLAY + ch.id);
+               return ChannelStatus::OFF;
+       }
+       else
+               return ChannelStatus::PLAY;
+}
+
+/* -------------------------------------------------------------------------- */
+
+ChannelStatus SampleReactor::pressWhilePlay(Channel& ch, Sequencer& sequencer,
+    SamplePlayerMode mode, bool isLoop) const
+{
+       if (isLoop)
+               return ChannelStatus::ENDING;
+
+       switch (mode)
+       {
+       case SamplePlayerMode::SINGLE_RETRIG:
+               if (sequencer.canQuantize())
+                       ch.shared->quantizer->trigger(Q_ACTION_REWIND + ch.id);
+               else
+                       rewind(*ch.shared, /*localFrame=*/0);
+               return ChannelStatus::PLAY;
+
+       case SamplePlayerMode::SINGLE_ENDLESS:
+               return ChannelStatus::ENDING;
+
+       case SamplePlayerMode::SINGLE_BASIC:
+               stop(*ch.shared);
+               return ChannelStatus::PLAY; // Let SamplePlayer stop it once done
+
+       default:
+               return ChannelStatus::OFF;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleReactor::press(Channel& ch, Sequencer& sequencer, int velocity) const
+{
+       const SamplePlayerMode mode   = ch.samplePlayer->mode;
+       const bool             isLoop = ch.samplePlayer->isAnyLoopMode();
+
+       ChannelStatus playStatus = ch.shared->playStatus.load();
+
+       switch (playStatus)
+       {
+       case ChannelStatus::OFF:
+               playStatus = pressWhileOff(ch, sequencer, velocity, isLoop);
+               break;
+
+       case ChannelStatus::PLAY:
+               playStatus = pressWhilePlay(ch, sequencer, mode, isLoop);
+               break;
+
+       case ChannelStatus::WAIT:
+               playStatus = ChannelStatus::OFF;
+               break;
+
+       case ChannelStatus::ENDING:
+               playStatus = ChannelStatus::PLAY;
+               break;
+
+       default:
+               break;
+       }
+
+       ch.shared->playStatus.store(playStatus);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleReactor::release(Channel& ch) const
+{
+       /* Key release is meaningful only for SINGLE_PRESS modes. */
+
+       if (ch.samplePlayer->mode != SamplePlayerMode::SINGLE_PRESS)
+               return;
+
+       /* Kill it if it's SINGLE_PRESS is playing. Otherwise there might be a 
+       quantization step in progress that would play the channel later on: 
+       disable it. */
+
+       if (ch.shared->playStatus.load() == ChannelStatus::PLAY)
+               stop(*ch.shared); // Let SamplePlayer stop it once done
+       else if (ch.shared->quantizer->hasBeenTriggered())
+               ch.shared->quantizer->clear();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void SampleReactor::onStopBySeq(Channel& ch, bool chansStopOnSeqHalt) const
+{
+       G_DEBUG("onStopBySeq ch=" << ch.id);
+
+       ChannelStatus playStatus       = ch.shared->playStatus.load();
+       bool          isReadingActions = ch.shared->readActions.load();
+       bool          isLoop           = ch.samplePlayer->isAnyLoopMode();
+
+       switch (playStatus)
+       {
+
+       case ChannelStatus::WAIT:
+               /* Loop-mode channels in wait status get stopped right away. */
+               if (isLoop)
+                       ch.shared->playStatus.store(ChannelStatus::OFF);
+               break;
+
+       case ChannelStatus::PLAY:
+               if (chansStopOnSeqHalt && (isLoop || isReadingActions))
+                       stop(*ch.shared);
+               break;
+
+       default:
+               break;
+       }
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/channels/sampleReactor.h b/src/core/channels/sampleReactor.h
new file mode 100644 (file)
index 0000000..0915779
--- /dev/null
@@ -0,0 +1,75 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_SAMPLE_REACTOR_H
+#define G_CHANNEL_SAMPLE_REACTOR_H
+
+#include "core/conf.h"
+#include "core/eventDispatcher.h"
+#include "core/quantizer.h"
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::m
+{
+class Channel;
+struct ChannelShared;
+class Sequencer;
+
+/* SampleReactor
+Reacts to manual events sent to Sample Channels: key press, key release, 
+sequencer stop, ... . */
+
+class SampleReactor final
+{
+public:
+       struct Event
+       {
+               int   type;
+               Frame offset;
+       };
+
+       SampleReactor(Channel&, ID channelId);
+
+       void react(Channel&, const EventDispatcher::Event&, Sequencer&, const Conf::Data&) const;
+
+private:
+       void          onStopBySeq(Channel&, bool chansStopOnSeqHalt) const;
+       void          release(Channel&) const;
+       void          press(Channel&, Sequencer&, int velocity) const;
+       ChannelStatus pressWhilePlay(Channel&, Sequencer&, SamplePlayerMode, bool isLoop) const;
+       ChannelStatus pressWhileOff(Channel&, Sequencer&, int velocity, bool isLoop) const;
+       void          rewind(ChannelShared&, Frame localFrame) const;
+       void          play(ChannelShared&, Frame localFrame) const;
+       void          stop(ChannelShared&) const;
+};
+
+} // namespace giada::m
+
+#endif
diff --git a/src/core/channels/waveReader.cpp b/src/core/channels/waveReader.cpp
new file mode 100644 (file)
index 0000000..bbc206d
--- /dev/null
@@ -0,0 +1,98 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "waveReader.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "core/wave.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include "utils/log.h"
+#include <algorithm>
+#include <cassert>
+#include <memory>
+
+namespace giada::m
+{
+WaveReader::WaveReader(Resampler* r)
+: wave(nullptr)
+, m_resampler(r)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+WaveReader::Result WaveReader::fill(mcl::AudioBuffer& out, Frame start, Frame max,
+    Frame offset, float pitch) const
+{
+       assert(wave != nullptr);
+       assert(start >= 0);
+       assert(max <= wave->getBuffer().countFrames());
+       assert(offset < out.countFrames());
+
+       if (pitch == 1.0f)
+               return fillCopy(out, start, max, offset);
+       else
+               return fillResampled(out, start, max, offset, pitch);
+}
+
+/* -------------------------------------------------------------------------- */
+
+WaveReader::Result WaveReader::fillResampled(mcl::AudioBuffer& dest, Frame start,
+    Frame max, Frame offset, float pitch) const
+{
+       Resampler::Result res = m_resampler->process(
+           /*input=*/wave->getBuffer()[0],
+           /*inputPos=*/start,
+           /*inputLen=*/max,
+           /*output=*/dest[offset],
+           /*outputLen=*/dest.countFrames() - offset,
+           /*pitch=*/pitch);
+
+       return {
+           static_cast<int>(res.used),
+           static_cast<int>(res.generated)};
+}
+
+/* -------------------------------------------------------------------------- */
+
+WaveReader::Result WaveReader::fillCopy(mcl::AudioBuffer& dest, Frame start,
+    Frame max, Frame offset) const
+{
+       Frame used = dest.countFrames() - offset;
+       if (used > max - start)
+               used = max - start;
+
+       dest.set(wave->getBuffer(), used, start, offset);
+
+       return {used, used};
+}
+
+void WaveReader::last() const
+{
+       if (m_resampler != nullptr)
+               m_resampler->last();
+}
+} // namespace giada::m
diff --git a/src/core/channels/waveReader.h b/src/core/channels/waveReader.h
new file mode 100644 (file)
index 0000000..d12dfff
--- /dev/null
@@ -0,0 +1,86 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CHANNEL_WAVE_READER_H
+#define G_CHANNEL_WAVE_READER_H
+
+#include "core/types.h"
+
+namespace mcl
+{
+class AudioBuffer;
+}
+
+namespace giada::m
+{
+class Wave;
+class Resampler;
+class WaveReader final
+{
+public:
+       /* Result
+       A Result object is returned by the fill() function below, containing the 
+       number of frames used and generated from a buffer filling operation. The
+       two values are different only when pitch is != 1.0, where a chunk of audio
+       in input (used) might result in a longer or shorter portion of audio in 
+       output (generated). */
+
+       struct Result
+       {
+               Frame used, generated;
+       };
+
+       WaveReader() = delete;
+       WaveReader(Resampler* r);
+
+       /* fill
+       Fills audio buffer 'out' with data coming from Wave, copying it from 'start'
+       frame up to 'max'. The buffer is filled starting at 'offset'. */
+
+       Result fill(mcl::AudioBuffer& out, Frame start, Frame max, Frame offset,
+           float pitch) const;
+
+       /* last
+       Call this when you are about to process the last chunk of pitched data. 
+       Ignored if pitch == 1.0. */
+
+       void last() const;
+
+       /* wave
+       Wave object. Might be null if the channel has no sample. */
+
+       Wave* wave;
+
+private:
+       Result fillResampled(mcl::AudioBuffer& out, Frame start, Frame max, Frame offset,
+           float pitch) const;
+       Result fillCopy(mcl::AudioBuffer& out, Frame start, Frame max, Frame offset) const;
+
+       Resampler* m_resampler;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/conf.cpp b/src/core/conf.cpp
new file mode 100644 (file)
index 0000000..ed38bf8
--- /dev/null
@@ -0,0 +1,316 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/types.h"
+#include "deps/json/single_include/nlohmann/json.hpp"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include <FL/Fl.H>
+#include <cassert>
+#include <fstream>
+#include <string>
+
+namespace nl = nlohmann;
+
+namespace giada::m
+{
+Conf::Conf()
+{
+       /* Initialize m_confFilePath, i.e. the configuration file. In windows it is 
+       in the same dir of the .exe, while in Linux and OS X in ~/.giada */
+
+#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__)
+
+       m_confFilePath = u::fs::getHomePath() + G_SLASH + CONF_FILENAME;
+       m_confDirPath  = u::fs::getHomePath() + G_SLASH;
+
+#elif defined(_WIN32)
+
+       m_confFilePath = CONF_FILENAME;
+       m_confDirPath  = "";
+
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Conf::read()
+{
+       data = Data(); // Reset it first
+
+       std::ifstream ifs(m_confFilePath);
+       if (!ifs.good())
+               return false;
+
+       nl::json j = nl::json::parse(ifs);
+
+       data.logMode                    = j.value(CONF_KEY_LOG_MODE, data.logMode);
+       data.showTooltips               = j.value(CONF_KEY_SHOW_TOOLTIPS, data.showTooltips);
+       data.soundSystem                = j.value(CONF_KEY_SOUND_SYSTEM, data.soundSystem);
+       data.soundDeviceOut             = j.value(CONF_KEY_SOUND_DEVICE_OUT, data.soundDeviceOut);
+       data.soundDeviceIn              = j.value(CONF_KEY_SOUND_DEVICE_IN, data.soundDeviceIn);
+       data.channelsOutCount           = j.value(CONF_KEY_CHANNELS_OUT_COUNT, data.channelsOutCount);
+       data.channelsOutStart           = j.value(CONF_KEY_CHANNELS_OUT_START, data.channelsOutStart);
+       data.channelsInCount            = j.value(CONF_KEY_CHANNELS_IN_COUNT, data.channelsInCount);
+       data.channelsInStart            = j.value(CONF_KEY_CHANNELS_IN_START, data.channelsInStart);
+       data.samplerate                 = j.value(CONF_KEY_SAMPLERATE, data.samplerate);
+       data.buffersize                 = j.value(CONF_KEY_BUFFER_SIZE, data.buffersize);
+       data.limitOutput                = j.value(CONF_KEY_LIMIT_OUTPUT, data.limitOutput);
+       data.rsmpQuality                = j.value(CONF_KEY_RESAMPLE_QUALITY, data.rsmpQuality);
+       data.midiSystem                 = j.value(CONF_KEY_MIDI_SYSTEM, data.midiSystem);
+       data.midiPortOut                = j.value(CONF_KEY_MIDI_PORT_OUT, data.midiPortOut);
+       data.midiPortIn                 = j.value(CONF_KEY_MIDI_PORT_IN, data.midiPortIn);
+       data.midiMapPath                = j.value(CONF_KEY_MIDIMAP_PATH, data.midiMapPath);
+       data.lastFileMap                = j.value(CONF_KEY_LAST_MIDIMAP, data.lastFileMap);
+       data.midiSync                   = j.value(CONF_KEY_MIDI_SYNC, data.midiSync);
+       data.midiTCfps                  = j.value(CONF_KEY_MIDI_TC_FPS, data.midiTCfps);
+       data.chansStopOnSeqHalt         = j.value(CONF_KEY_CHANS_STOP_ON_SEQ_HALT, data.chansStopOnSeqHalt);
+       data.treatRecsAsLoops           = j.value(CONF_KEY_TREAT_RECS_AS_LOOPS, data.treatRecsAsLoops);
+       data.inputMonitorDefaultOn      = j.value(CONF_KEY_INPUT_MONITOR_DEFAULT_ON, data.inputMonitorDefaultOn);
+       data.overdubProtectionDefaultOn = j.value(CONF_KEY_OVERDUB_PROTECTION_DEFAULT_ON, data.overdubProtectionDefaultOn);
+       data.pluginPath                 = j.value(CONF_KEY_PLUGINS_PATH, data.pluginPath);
+       data.patchPath                  = j.value(CONF_KEY_PATCHES_PATH, data.patchPath);
+       data.samplePath                 = j.value(CONF_KEY_SAMPLES_PATH, data.samplePath);
+       data.mainWindowX                = j.value(CONF_KEY_MAIN_WINDOW_X, data.mainWindowX);
+       data.mainWindowY                = j.value(CONF_KEY_MAIN_WINDOW_Y, data.mainWindowY);
+       data.mainWindowW                = j.value(CONF_KEY_MAIN_WINDOW_W, data.mainWindowW);
+       data.mainWindowH                = j.value(CONF_KEY_MAIN_WINDOW_H, data.mainWindowH);
+       data.browserX                   = j.value(CONF_KEY_BROWSER_X, data.browserX);
+       data.browserY                   = j.value(CONF_KEY_BROWSER_Y, data.browserY);
+       data.browserW                   = j.value(CONF_KEY_BROWSER_W, data.browserW);
+       data.browserH                   = j.value(CONF_KEY_BROWSER_H, data.browserH);
+       data.browserPosition            = j.value(CONF_KEY_BROWSER_POSITION, data.browserPosition);
+       data.browserLastPath            = j.value(CONF_KEY_BROWSER_LAST_PATH, data.browserLastPath);
+       data.browserLastValue           = j.value(CONF_KEY_BROWSER_LAST_VALUE, data.browserLastValue);
+       data.actionEditorX              = j.value(CONF_KEY_ACTION_EDITOR_X, data.actionEditorX);
+       data.actionEditorY              = j.value(CONF_KEY_ACTION_EDITOR_Y, data.actionEditorY);
+       data.actionEditorW              = j.value(CONF_KEY_ACTION_EDITOR_W, data.actionEditorW);
+       data.actionEditorH              = j.value(CONF_KEY_ACTION_EDITOR_H, data.actionEditorH);
+       data.actionEditorZoom           = j.value(CONF_KEY_ACTION_EDITOR_ZOOM, data.actionEditorZoom);
+       data.actionEditorSplitH         = j.value(CONF_KEY_ACTION_EDITOR_SPLIT_H, data.actionEditorSplitH);
+       data.actionEditorGridVal        = j.value(CONF_KEY_ACTION_EDITOR_GRID_VAL, data.actionEditorGridVal);
+       data.actionEditorGridOn         = j.value(CONF_KEY_ACTION_EDITOR_GRID_ON, data.actionEditorGridOn);
+       data.actionEditorPianoRollY     = j.value(CONF_KEY_ACTION_EDITOR_PIANO_ROLL_Y, data.actionEditorPianoRollY);
+       data.sampleEditorX              = j.value(CONF_KEY_SAMPLE_EDITOR_X, data.sampleEditorX);
+       data.sampleEditorY              = j.value(CONF_KEY_SAMPLE_EDITOR_Y, data.sampleEditorY);
+       data.sampleEditorW              = j.value(CONF_KEY_SAMPLE_EDITOR_W, data.sampleEditorW);
+       data.sampleEditorH              = j.value(CONF_KEY_SAMPLE_EDITOR_H, data.sampleEditorH);
+       data.sampleEditorGridVal        = j.value(CONF_KEY_SAMPLE_EDITOR_GRID_VAL, data.sampleEditorGridVal);
+       data.sampleEditorGridOn         = j.value(CONF_KEY_SAMPLE_EDITOR_GRID_ON, data.sampleEditorGridOn);
+       data.pluginListX                = j.value(CONF_KEY_PLUGIN_LIST_X, data.pluginListX);
+       data.pluginListY                = j.value(CONF_KEY_PLUGIN_LIST_Y, data.pluginListY);
+       data.midiInputX                 = j.value(CONF_KEY_MIDI_INPUT_X, data.midiInputX);
+       data.midiInputY                 = j.value(CONF_KEY_MIDI_INPUT_Y, data.midiInputY);
+       data.midiInputW                 = j.value(CONF_KEY_MIDI_INPUT_W, data.midiInputW);
+       data.midiInputH                 = j.value(CONF_KEY_MIDI_INPUT_H, data.midiInputH);
+       data.recTriggerMode             = j.value(CONF_KEY_REC_TRIGGER_MODE, data.recTriggerMode);
+       data.recTriggerLevel            = j.value(CONF_KEY_REC_TRIGGER_LEVEL, data.recTriggerLevel);
+       data.inputRecMode               = j.value(CONF_KEY_INPUT_REC_MODE, data.inputRecMode);
+       data.midiInEnabled              = j.value(CONF_KEY_MIDI_IN, data.midiInEnabled);
+       data.midiInFilter               = j.value(CONF_KEY_MIDI_IN_FILTER, data.midiInFilter);
+       data.midiInRewind               = j.value(CONF_KEY_MIDI_IN_REWIND, data.midiInRewind);
+       data.midiInStartStop            = j.value(CONF_KEY_MIDI_IN_START_STOP, data.midiInStartStop);
+       data.midiInActionRec            = j.value(CONF_KEY_MIDI_IN_ACTION_REC, data.midiInActionRec);
+       data.midiInInputRec             = j.value(CONF_KEY_MIDI_IN_INPUT_REC, data.midiInInputRec);
+       data.midiInMetronome            = j.value(CONF_KEY_MIDI_IN_METRONOME, data.midiInMetronome);
+       data.midiInVolumeIn             = j.value(CONF_KEY_MIDI_IN_VOLUME_IN, data.midiInVolumeIn);
+       data.midiInVolumeOut            = j.value(CONF_KEY_MIDI_IN_VOLUME_OUT, data.midiInVolumeOut);
+       data.midiInBeatDouble           = j.value(CONF_KEY_MIDI_IN_BEAT_DOUBLE, data.midiInBeatDouble);
+       data.midiInBeatHalf             = j.value(CONF_KEY_MIDI_IN_BEAT_HALF, data.midiInBeatHalf);
+
+       data.keyBindings[KEY_BIND_PLAY]           = j.value(CONF_KEY_BIND_PLAY, 0);
+       data.keyBindings[KEY_BIND_REWIND]         = j.value(CONF_KEY_BIND_REWIND, 0);
+       data.keyBindings[KEY_BIND_RECORD_ACTIONS] = j.value(CONF_KEY_BIND_RECORD_ACTIONS, 0);
+       data.keyBindings[KEY_BIND_RECORD_INPUT]   = j.value(CONF_KEY_BIND_RECORD_INPUT, 0);
+       data.keyBindings[KEY_BIND_EXIT]           = j.value(CONF_KEY_BIND_EXIT, 0);
+
+#ifdef WITH_VST
+       data.pluginChooserX   = j.value(CONF_KEY_PLUGIN_CHOOSER_X, data.pluginChooserX);
+       data.pluginChooserY   = j.value(CONF_KEY_PLUGIN_CHOOSER_Y, data.pluginChooserY);
+       data.pluginChooserW   = j.value(CONF_KEY_PLUGIN_CHOOSER_W, data.pluginChooserW);
+       data.pluginChooserH   = j.value(CONF_KEY_PLUGIN_CHOOSER_H, data.pluginChooserH);
+       data.pluginSortMethod = j.value(CONF_KEY_PLUGIN_SORT_METHOD, data.pluginSortMethod);
+#endif
+
+       sanitize();
+
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Conf::write() const
+{
+       if (!createConfigFolder())
+               return false;
+
+       nl::json j;
+
+       j[CONF_KEY_HEADER]                        = "GIADACFG";
+       j[CONF_KEY_LOG_MODE]                      = data.logMode;
+       j[CONF_KEY_SHOW_TOOLTIPS]                 = data.showTooltips;
+       j[CONF_KEY_SOUND_SYSTEM]                  = data.soundSystem;
+       j[CONF_KEY_SOUND_DEVICE_OUT]              = data.soundDeviceOut;
+       j[CONF_KEY_SOUND_DEVICE_IN]               = data.soundDeviceIn;
+       j[CONF_KEY_CHANNELS_OUT_COUNT]            = data.channelsOutCount;
+       j[CONF_KEY_CHANNELS_OUT_START]            = data.channelsOutStart;
+       j[CONF_KEY_CHANNELS_IN_COUNT]             = data.channelsInCount;
+       j[CONF_KEY_CHANNELS_IN_START]             = data.channelsInStart;
+       j[CONF_KEY_SAMPLERATE]                    = data.samplerate;
+       j[CONF_KEY_BUFFER_SIZE]                   = data.buffersize;
+       j[CONF_KEY_LIMIT_OUTPUT]                  = data.limitOutput;
+       j[CONF_KEY_RESAMPLE_QUALITY]              = data.rsmpQuality;
+       j[CONF_KEY_MIDI_SYSTEM]                   = data.midiSystem;
+       j[CONF_KEY_MIDI_PORT_OUT]                 = data.midiPortOut;
+       j[CONF_KEY_MIDI_PORT_IN]                  = data.midiPortIn;
+       j[CONF_KEY_MIDIMAP_PATH]                  = data.midiMapPath;
+       j[CONF_KEY_LAST_MIDIMAP]                  = data.lastFileMap;
+       j[CONF_KEY_MIDI_SYNC]                     = data.midiSync;
+       j[CONF_KEY_MIDI_TC_FPS]                   = data.midiTCfps;
+       j[CONF_KEY_MIDI_IN]                       = data.midiInEnabled;
+       j[CONF_KEY_MIDI_IN_FILTER]                = data.midiInFilter;
+       j[CONF_KEY_MIDI_IN_REWIND]                = data.midiInRewind;
+       j[CONF_KEY_MIDI_IN_START_STOP]            = data.midiInStartStop;
+       j[CONF_KEY_MIDI_IN_ACTION_REC]            = data.midiInActionRec;
+       j[CONF_KEY_MIDI_IN_INPUT_REC]             = data.midiInInputRec;
+       j[CONF_KEY_MIDI_IN_METRONOME]             = data.midiInMetronome;
+       j[CONF_KEY_MIDI_IN_VOLUME_IN]             = data.midiInVolumeIn;
+       j[CONF_KEY_MIDI_IN_VOLUME_OUT]            = data.midiInVolumeOut;
+       j[CONF_KEY_MIDI_IN_BEAT_DOUBLE]           = data.midiInBeatDouble;
+       j[CONF_KEY_MIDI_IN_BEAT_HALF]             = data.midiInBeatHalf;
+       j[CONF_KEY_CHANS_STOP_ON_SEQ_HALT]        = data.chansStopOnSeqHalt;
+       j[CONF_KEY_TREAT_RECS_AS_LOOPS]           = data.treatRecsAsLoops;
+       j[CONF_KEY_INPUT_MONITOR_DEFAULT_ON]      = data.inputMonitorDefaultOn;
+       j[CONF_KEY_OVERDUB_PROTECTION_DEFAULT_ON] = data.overdubProtectionDefaultOn;
+       j[CONF_KEY_PLUGINS_PATH]                  = data.pluginPath;
+       j[CONF_KEY_PATCHES_PATH]                  = data.patchPath;
+       j[CONF_KEY_SAMPLES_PATH]                  = data.samplePath;
+       j[CONF_KEY_MAIN_WINDOW_X]                 = data.mainWindowX;
+       j[CONF_KEY_MAIN_WINDOW_Y]                 = data.mainWindowY;
+       j[CONF_KEY_MAIN_WINDOW_W]                 = data.mainWindowW;
+       j[CONF_KEY_MAIN_WINDOW_H]                 = data.mainWindowH;
+       j[CONF_KEY_BROWSER_X]                     = data.browserX;
+       j[CONF_KEY_BROWSER_Y]                     = data.browserY;
+       j[CONF_KEY_BROWSER_W]                     = data.browserW;
+       j[CONF_KEY_BROWSER_H]                     = data.browserH;
+       j[CONF_KEY_BROWSER_POSITION]              = data.browserPosition;
+       j[CONF_KEY_BROWSER_LAST_PATH]             = data.browserLastPath;
+       j[CONF_KEY_BROWSER_LAST_VALUE]            = data.browserLastValue;
+       j[CONF_KEY_ACTION_EDITOR_X]               = data.actionEditorX;
+       j[CONF_KEY_ACTION_EDITOR_Y]               = data.actionEditorY;
+       j[CONF_KEY_ACTION_EDITOR_W]               = data.actionEditorW;
+       j[CONF_KEY_ACTION_EDITOR_H]               = data.actionEditorH;
+       j[CONF_KEY_ACTION_EDITOR_ZOOM]            = data.actionEditorZoom;
+       j[CONF_KEY_ACTION_EDITOR_SPLIT_H]         = data.actionEditorSplitH;
+       j[CONF_KEY_ACTION_EDITOR_GRID_VAL]        = data.actionEditorGridVal;
+       j[CONF_KEY_ACTION_EDITOR_GRID_ON]         = data.actionEditorGridOn;
+       j[CONF_KEY_ACTION_EDITOR_PIANO_ROLL_Y]    = data.actionEditorPianoRollY;
+       j[CONF_KEY_SAMPLE_EDITOR_X]               = data.sampleEditorX;
+       j[CONF_KEY_SAMPLE_EDITOR_Y]               = data.sampleEditorY;
+       j[CONF_KEY_SAMPLE_EDITOR_W]               = data.sampleEditorW;
+       j[CONF_KEY_SAMPLE_EDITOR_H]               = data.sampleEditorH;
+       j[CONF_KEY_SAMPLE_EDITOR_GRID_VAL]        = data.sampleEditorGridVal;
+       j[CONF_KEY_SAMPLE_EDITOR_GRID_ON]         = data.sampleEditorGridOn;
+       j[CONF_KEY_PLUGIN_LIST_X]                 = data.pluginListX;
+       j[CONF_KEY_PLUGIN_LIST_Y]                 = data.pluginListY;
+       j[CONF_KEY_MIDI_INPUT_X]                  = data.midiInputX;
+       j[CONF_KEY_MIDI_INPUT_Y]                  = data.midiInputY;
+       j[CONF_KEY_MIDI_INPUT_W]                  = data.midiInputW;
+       j[CONF_KEY_MIDI_INPUT_H]                  = data.midiInputH;
+       j[CONF_KEY_REC_TRIGGER_MODE]              = static_cast<int>(data.recTriggerMode);
+       j[CONF_KEY_REC_TRIGGER_LEVEL]             = data.recTriggerLevel;
+       j[CONF_KEY_INPUT_REC_MODE]                = static_cast<int>(data.inputRecMode);
+
+       j[CONF_KEY_BIND_PLAY]           = data.keyBindings.find(KEY_BIND_PLAY)->second;
+       j[CONF_KEY_BIND_REWIND]         = data.keyBindings.find(KEY_BIND_REWIND)->second;
+       j[CONF_KEY_BIND_RECORD_ACTIONS] = data.keyBindings.find(KEY_BIND_RECORD_ACTIONS)->second;
+       j[CONF_KEY_BIND_RECORD_INPUT]   = data.keyBindings.find(KEY_BIND_RECORD_INPUT)->second;
+       j[CONF_KEY_BIND_EXIT]           = data.keyBindings.find(KEY_BIND_EXIT)->second;
+
+#ifdef WITH_VST
+       j[CONF_KEY_PLUGIN_CHOOSER_X]   = data.pluginChooserX;
+       j[CONF_KEY_PLUGIN_CHOOSER_Y]   = data.pluginChooserY;
+       j[CONF_KEY_PLUGIN_CHOOSER_W]   = data.pluginChooserW;
+       j[CONF_KEY_PLUGIN_CHOOSER_H]   = data.pluginChooserH;
+       j[CONF_KEY_PLUGIN_SORT_METHOD] = data.pluginSortMethod;
+#endif
+
+       std::ofstream ofs(m_confFilePath);
+       if (!ofs.good())
+       {
+               u::log::print("[conf::write] unable to write configuration file!\n");
+               return false;
+       }
+
+       ofs << j;
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Conf::createConfigFolder() const
+{
+#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__)
+
+       if (u::fs::dirExists(m_confDirPath))
+               return true;
+
+       u::log::print("[conf::createConfigFolder] .giada folder not present. Updating...\n");
+
+       if (u::fs::mkdir(m_confDirPath))
+       {
+               u::log::print("[conf::createConfigFolder] status: ok\n");
+               return true;
+       }
+       else
+       {
+               u::log::print("[conf::createConfigFolder] status: error!\n");
+               return false;
+       }
+
+#else // Windows: nothing to do
+
+       return true;
+
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Conf::sanitize()
+{
+       data.soundDeviceOut   = std::max(0, data.soundDeviceOut);
+       data.channelsOutCount = G_MAX_IO_CHANS;
+       data.channelsOutStart = std::max(0, data.channelsOutStart);
+       data.channelsInCount  = std::max(1, data.channelsInCount);
+       data.channelsInStart  = std::max(0, data.channelsInStart);
+
+       data.midiPortOut = std::max(-1, data.midiPortOut);
+       data.midiPortIn  = std::max(-1, data.midiPortIn);
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/conf.h b/src/core/conf.h
new file mode 100644 (file)
index 0000000..8541878
--- /dev/null
@@ -0,0 +1,175 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CONF_H
+#define G_CONF_H
+
+#include "core/const.h"
+#include "core/types.h"
+#include "utils/gui.h"
+#include <string>
+#include <unordered_map>
+
+namespace giada::m
+{
+class Conf final
+{
+public:
+       using KeyBindings = std::unordered_map<int, int>;
+
+       static constexpr int KEY_BIND_PLAY           = 1;
+       static constexpr int KEY_BIND_REWIND         = 2;
+       static constexpr int KEY_BIND_RECORD_ACTIONS = 3;
+       static constexpr int KEY_BIND_RECORD_INPUT   = 4;
+       static constexpr int KEY_BIND_EXIT           = 5;
+
+       struct Data
+       {
+               int  logMode          = LOG_MODE_MUTE;
+               bool showTooltips     = true;
+               int  soundSystem      = G_DEFAULT_SOUNDSYS;
+               int  soundDeviceOut   = G_DEFAULT_SOUNDDEV_OUT;
+               int  soundDeviceIn    = G_DEFAULT_SOUNDDEV_IN;
+               int  channelsOutCount = G_MAX_IO_CHANS;
+               int  channelsOutStart = 0;
+               int  channelsInCount  = 1;
+               int  channelsInStart  = 0;
+               int  samplerate       = G_DEFAULT_SAMPLERATE;
+               int  buffersize       = G_DEFAULT_BUFSIZE;
+               bool limitOutput      = false;
+               int  rsmpQuality      = 0;
+
+               int         midiSystem  = 0;
+               int         midiPortOut = G_DEFAULT_MIDI_PORT_OUT;
+               int         midiPortIn  = G_DEFAULT_MIDI_PORT_IN;
+               std::string midiMapPath = "";
+               std::string lastFileMap = "";
+               int         midiSync    = G_MIDI_SYNC_NONE;
+               float       midiTCfps   = 25.0f;
+
+               bool chansStopOnSeqHalt         = false;
+               bool treatRecsAsLoops           = false;
+               bool inputMonitorDefaultOn      = false;
+               bool overdubProtectionDefaultOn = false;
+
+               std::string pluginPath;
+               std::string patchPath;
+               std::string samplePath;
+
+               int mainWindowX = u::gui::centerWindowX(G_MIN_GUI_WIDTH);
+               int mainWindowY = u::gui::centerWindowY(G_MIN_GUI_HEIGHT);
+               int mainWindowW = G_MIN_GUI_WIDTH;
+               int mainWindowH = G_MIN_GUI_HEIGHT;
+
+               int         browserX = u::gui::centerWindowX(G_DEFAULT_SUBWINDOW_W);
+               int         browserY = u::gui::centerWindowY(G_DEFAULT_SUBWINDOW_H);
+               int         browserW = G_DEFAULT_SUBWINDOW_W;
+               int         browserH = G_DEFAULT_SUBWINDOW_H;
+               int         browserPosition;
+               int         browserLastValue;
+               std::string browserLastPath;
+
+               int actionEditorY          = u::gui::centerWindowY(G_DEFAULT_SUBWINDOW_H);
+               int actionEditorX          = u::gui::centerWindowX(G_DEFAULT_SUBWINDOW_W);
+               int actionEditorW          = G_DEFAULT_SUBWINDOW_W;
+               int actionEditorH          = G_DEFAULT_SUBWINDOW_H;
+               int actionEditorZoom       = G_DEFAULT_ZOOM_RATIO;
+               int actionEditorSplitH     = -1;
+               int actionEditorGridVal    = 0;
+               int actionEditorGridOn     = false;
+               int actionEditorPianoRollY = -1;
+
+               int sampleEditorX;
+               int sampleEditorY;
+               int sampleEditorW       = G_DEFAULT_SUBWINDOW_W;
+               int sampleEditorH       = G_DEFAULT_SUBWINDOW_H;
+               int sampleEditorGridVal = 0;
+               int sampleEditorGridOn  = false;
+
+               int midiInputX;
+               int midiInputY;
+               int midiInputW = G_DEFAULT_SUBWINDOW_W;
+               int midiInputH = G_DEFAULT_SUBWINDOW_H;
+
+               int pluginListX;
+               int pluginListY;
+
+               RecTriggerMode recTriggerMode  = RecTriggerMode::NORMAL;
+               float          recTriggerLevel = G_DEFAULT_REC_TRIGGER_LEVEL;
+               InputRecMode   inputRecMode    = InputRecMode::FREE;
+
+               bool     midiInEnabled    = false;
+               int      midiInFilter     = -1;
+               uint32_t midiInRewind     = 0x0;
+               uint32_t midiInStartStop  = 0x0;
+               uint32_t midiInActionRec  = 0x0;
+               uint32_t midiInInputRec   = 0x0;
+               uint32_t midiInMetronome  = 0x0;
+               uint32_t midiInVolumeIn   = 0x0;
+               uint32_t midiInVolumeOut  = 0x0;
+               uint32_t midiInBeatDouble = 0x0;
+               uint32_t midiInBeatHalf   = 0x0;
+
+#ifdef WITH_VST
+
+               int pluginChooserX;
+               int pluginChooserY;
+               int pluginChooserW   = G_DEFAULT_SUBWINDOW_W;
+               int pluginChooserH   = G_DEFAULT_SUBWINDOW_H;
+               int pluginSortMethod = 0;
+
+#endif
+
+               KeyBindings keyBindings = {
+                   {KEY_BIND_PLAY, ' '},
+                   {KEY_BIND_REWIND, FL_BackSpace},
+                   {KEY_BIND_RECORD_ACTIONS, FL_Enter},
+                   {KEY_BIND_RECORD_INPUT, FL_End},
+                   {KEY_BIND_EXIT, FL_Escape}};
+       };
+
+       Conf();
+
+       bool read();
+       bool write() const;
+
+       Data data;
+
+private:
+       /* createConfigFolder
+       Creates local folder where to put the configuration file. Path differs from 
+       OS to OS. */
+
+       bool createConfigFolder() const;
+
+       void sanitize();
+
+       std::string m_confFilePath;
+       std::string m_confDirPath;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/const.h b/src/core/const.h
new file mode 100644 (file)
index 0000000..db9bc5b
--- /dev/null
@@ -0,0 +1,470 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_CONST_H
+#define G_CONST_H
+
+#include <cstdint>
+#include <iostream>
+
+/* -- debug ----------------------------------------------------------------- */
+#ifndef NDEBUG
+#define G_DEBUG_MODE
+// TODO - move G_DEBUG macro definition to u::log
+#define G_DEBUG(x) std::cerr << __FILE__ << "::" << __func__ << "() - " << x << "\n";
+#else
+#define G_DEBUG(x) \
+       do             \
+       {              \
+       } while (0)
+#endif
+
+/* -- environment ----------------------------------------------------------- */
+#if defined(_WIN32)
+#define G_OS_WINDOWS
+#elif defined(__APPLE__)
+#define G_OS_MAC
+#elif defined(__linux__)
+#define G_OS_LINUX
+#elif defined(__FreeBSD__)
+#define G_OS_FREEBSD
+#endif
+
+#ifndef BUILD_DATE
+#define BUILD_DATE __DATE__
+#endif
+
+/* -- version --------------------------------------------------------------- */
+constexpr auto G_APP_NAME      = "Giada";
+constexpr auto G_VERSION_STR   = "0.21.0";
+constexpr int  G_VERSION_MAJOR = 0;
+constexpr int  G_VERSION_MINOR = 21;
+constexpr int  G_VERSION_PATCH = 0;
+
+constexpr auto CONF_FILENAME = "giada.conf";
+
+#ifdef G_OS_WINDOWS
+#define G_SLASH '\\'
+#define G_SLASH_STR "\\"
+#else
+#define G_SLASH '/'
+#define G_SLASH_STR "/"
+#endif
+
+/* -- Engine ---------------------------------------------------------------- */
+/* G_EVENT_DISPATCHER_RATE_MS
+The amount of sleep between each Event Dispatcher cycle. It should be lower
+than the audio thread sleep time. Note: this value will obviously increase the 
+live input latency, keep it small! */
+constexpr int G_EVENT_DISPATCHER_RATE_MS = 5;
+
+/* -- GUI ------------------------------------------------------------------- */
+constexpr int   G_GUI_FPS            = 30;
+constexpr float G_GUI_REFRESH_RATE   = 1 / static_cast<float>(G_GUI_FPS);
+constexpr int   G_GUI_FONT_SIZE_BASE = 12;
+constexpr int   G_GUI_INNER_MARGIN   = 4;
+constexpr int   G_GUI_OUTER_MARGIN   = 8;
+constexpr int   G_GUI_UNIT           = 20; // base unit for elements
+constexpr int   G_GUI_ZOOM_FACTOR    = 2;
+
+#define G_COLOR_RED fl_rgb_color(28, 32, 80)
+#define G_COLOR_BLUE fl_rgb_color(113, 31, 31)
+#define G_COLOR_LIGHT_2 fl_rgb_color(200, 200, 200)
+#define G_COLOR_LIGHT_1 fl_rgb_color(170, 170, 170)
+#define G_COLOR_GREY_4 fl_rgb_color(78, 78, 78)
+#define G_COLOR_GREY_3 fl_rgb_color(54, 54, 54)
+#define G_COLOR_GREY_2 fl_rgb_color(37, 37, 37)
+#define G_COLOR_GREY_1_5 fl_rgb_color(28, 28, 28)
+#define G_COLOR_GREY_1 fl_rgb_color(25, 25, 25)
+#define G_COLOR_BLACK fl_rgb_color(0, 0, 0)
+
+/* -- MIN/MAX values -------------------------------------------------------- */
+constexpr float G_MIN_BPM               = 20.0f;
+constexpr auto  G_MIN_BPM_STR           = "20.0";
+constexpr float G_MAX_BPM               = 999.0f;
+constexpr auto  G_MAX_BPM_STR           = "999.0";
+constexpr int   G_MAX_BEATS             = 32;
+constexpr int   G_MAX_BARS              = 32;
+constexpr int   G_MAX_QUANTIZE          = 8;
+constexpr float G_MIN_DB_SCALE          = 60.0f;
+constexpr int   G_MIN_COLUMN_WIDTH      = 140;
+constexpr float G_MAX_BOOST_DB          = 20.0f;
+constexpr float G_MIN_PITCH             = 0.1f;
+constexpr float G_MAX_PITCH             = 4.0f;
+constexpr float G_MAX_PAN               = 1.0f;
+constexpr float G_MAX_VOLUME            = 1.0f;
+constexpr int   G_MIN_GUI_WIDTH         = 816;
+constexpr int   G_MIN_GUI_HEIGHT        = 510;
+constexpr int   G_MAX_IO_CHANS          = 2;
+constexpr int   G_MAX_VELOCITY          = 0x7F;
+constexpr int   G_MAX_MIDI_CHANS        = 16;
+constexpr int   G_MAX_DISPATCHER_EVENTS = 32;
+constexpr int   G_MAX_SEQUENCER_EVENTS  = 128; // Per block
+
+/* -- kernel audio ---------------------------------------------------------- */
+constexpr int G_SYS_API_NONE   = 0;
+constexpr int G_SYS_API_JACK   = 1;
+constexpr int G_SYS_API_ALSA   = 2;
+constexpr int G_SYS_API_DS     = 3;
+constexpr int G_SYS_API_ASIO   = 4;
+constexpr int G_SYS_API_CORE   = 5;
+constexpr int G_SYS_API_PULSE  = 6;
+constexpr int G_SYS_API_WASAPI = 7;
+
+/* -- kernel midi ----------------------------------------------------------- */
+constexpr int G_MIDI_API_JACK = 1;
+constexpr int G_MIDI_API_ALSA = 2;
+constexpr int G_MIDI_API_MM   = 3;
+constexpr int G_MIDI_API_CORE = 4;
+
+/* -- default system -------------------------------------------------------- */
+#if defined(G_OS_LINUX)
+constexpr int G_DEFAULT_SOUNDSYS = G_SYS_API_NONE;
+#elif defined(G_OS_FREEBSD)
+constexpr int G_DEFAULT_SOUNDSYS = G_SYS_API_PULSE;
+#elif defined(G_OS_WINDOWS)
+constexpr int G_DEFAULT_SOUNDSYS = G_SYS_API_DS;
+#elif defined(G_OS_MAC)
+constexpr int G_DEFAULT_SOUNDSYS = G_SYS_API_CORE;
+#endif
+
+constexpr int   G_DEFAULT_SOUNDDEV_OUT        = -1; // disabled by default
+constexpr int   G_DEFAULT_SOUNDDEV_IN         = -1; // disabled by default
+constexpr int   G_DEFAULT_MIDI_SYSTEM         = 0;
+constexpr int   G_DEFAULT_MIDI_PORT_IN        = -1;
+constexpr int   G_DEFAULT_MIDI_PORT_OUT       = -1;
+constexpr int   G_DEFAULT_SAMPLERATE          = 44100;
+constexpr int   G_DEFAULT_BUFSIZE             = 1024;
+constexpr int   G_DEFAULT_BIT_DEPTH           = 32;
+constexpr float G_DEFAULT_VOL                 = 1.0f;
+constexpr float G_DEFAULT_PAN                 = 0.5f;
+constexpr float G_DEFAULT_PITCH               = 1.0f;
+constexpr float G_DEFAULT_BPM                 = 120.0f;
+constexpr int   G_DEFAULT_BEATS               = 4;
+constexpr int   G_DEFAULT_BARS                = 1;
+constexpr int   G_DEFAULT_QUANTIZE            = 0;     // quantizer off
+constexpr float G_DEFAULT_FADEOUT_STEP        = 0.01f; // micro-fadeout speed
+constexpr int   G_DEFAULT_COLUMN_WIDTH        = 380;
+constexpr auto  G_DEFAULT_PATCH_NAME          = "(default patch)";
+constexpr int   G_DEFAULT_ACTION_SIZE         = 8192; // frames
+constexpr int   G_DEFAULT_ZOOM_RATIO          = 128;
+constexpr float G_DEFAULT_REC_TRIGGER_LEVEL   = -10.0f;
+constexpr int   G_DEFAULT_SUBWINDOW_W         = 640;
+constexpr int   G_DEFAULT_SUBWINDOW_H         = 480;
+constexpr int   G_DEFAULT_VST_MIDIBUFFER_SIZE = 1024; // TODO - not 100% sure about this size
+
+/* -- responses and return codes -------------------------------------------- */
+constexpr int G_RES_ERR_PROCESSING    = -6;
+constexpr int G_RES_ERR_WRONG_DATA    = -5;
+constexpr int G_RES_ERR_NO_DATA       = -4;
+constexpr int G_RES_ERR_PATH_TOO_LONG = -3;
+constexpr int G_RES_ERR_IO            = -2;
+constexpr int G_RES_ERR_MEMORY        = -1;
+constexpr int G_RES_ERR               = 0;
+constexpr int G_RES_OK                = 1;
+
+/* -- log modes ------------------------------------------------------------- */
+constexpr int LOG_MODE_MUTE   = 0;
+constexpr int LOG_MODE_STDOUT = 1;
+constexpr int LOG_MODE_FILE   = 2;
+
+/* -- unique IDs of mainWin's subwindows ------------------------------------ */
+/* -- wid > 0 are reserved by gg_keyboard ----------------------------------- */
+constexpr int WID_BEATS          = -1;
+constexpr int WID_BPM            = -2;
+constexpr int WID_ABOUT          = -3;
+constexpr int WID_FILE_BROWSER   = -4;
+constexpr int WID_CONFIG         = -5;
+constexpr int WID_FX_LIST        = -6;
+constexpr int WID_ACTION_EDITOR  = -7;
+constexpr int WID_SAMPLE_EDITOR  = -8;
+constexpr int WID_FX             = -9;
+constexpr int WID_KEY_GRABBER    = -10;
+constexpr int WID_SAMPLE_NAME    = -11;
+constexpr int WID_FX_CHOOSER     = -12;
+constexpr int WID_MIDI_INPUT     = -13;
+constexpr int WID_MIDI_OUTPUT    = -14;
+constexpr int WID_MISSING_ASSETS = -15;
+
+/* -- patch signals --------------------------------------------------------- */
+constexpr int G_PATCH_UNSUPPORTED = -2;
+constexpr int G_PATCH_UNREADABLE  = -1;
+constexpr int G_PATCH_INVALID     = 0;
+constexpr int G_PATCH_OK          = 1;
+
+/* -- midimap signals ------------------------------------------------------- */
+constexpr int MIDIMAP_NOT_SPECIFIED = 0x00;
+constexpr int MIDIMAP_UNREADABLE    = 0x01;
+constexpr int MIDIMAP_INVALID       = 0x02;
+constexpr int MIDIMAP_READ_OK       = 0x04;
+
+/* -- MIDI in parameters (for MIDI learning) -------------------------------- */
+constexpr int G_MIDI_IN_ENABLED      = 1;
+constexpr int G_MIDI_IN_FILTER       = 2;
+constexpr int G_MIDI_IN_REWIND       = 3;
+constexpr int G_MIDI_IN_START_STOP   = 4;
+constexpr int G_MIDI_IN_ACTION_REC   = 5;
+constexpr int G_MIDI_IN_INPUT_REC    = 6;
+constexpr int G_MIDI_IN_METRONOME    = 7;
+constexpr int G_MIDI_IN_VOLUME_IN    = 8;
+constexpr int G_MIDI_IN_VOLUME_OUT   = 9;
+constexpr int G_MIDI_IN_BEAT_DOUBLE  = 10;
+constexpr int G_MIDI_IN_BEAT_HALF    = 11;
+constexpr int G_MIDI_IN_KEYPRESS     = 12;
+constexpr int G_MIDI_IN_KEYREL       = 13;
+constexpr int G_MIDI_IN_KILL         = 14;
+constexpr int G_MIDI_IN_ARM          = 15;
+constexpr int G_MIDI_IN_MUTE         = 16;
+constexpr int G_MIDI_IN_SOLO         = 17;
+constexpr int G_MIDI_IN_VOLUME       = 18;
+constexpr int G_MIDI_IN_PITCH        = 19;
+constexpr int G_MIDI_IN_READ_ACTIONS = 20;
+
+/* -- MIDI out parameters (for MIDI output and lightning) ------------------- */
+constexpr int G_MIDI_OUT_ENABLED   = 1;
+constexpr int G_MIDI_OUT_L_ENABLED = 2;
+constexpr int G_MIDI_OUT_L_PLAYING = 3;
+constexpr int G_MIDI_OUT_L_MUTE    = 4;
+constexpr int G_MIDI_OUT_L_SOLO    = 5;
+
+/* -- MIDI signals -------------------------------------------------------------
+Channel voices messages - controller (0xB0) is a special subset of this family:
+it drives knobs, volume, faders and such. */
+
+constexpr uint32_t G_MIDI_CONTROLLER    = static_cast<uint32_t>(0xB0 << 24);
+constexpr uint32_t G_MIDI_ALL_NOTES_OFF = (G_MIDI_CONTROLLER) | (0x7B << 16);
+
+/* system common / real-time messages. Single bytes */
+
+constexpr int MIDI_SYSEX        = 0xF0;
+constexpr int MIDI_MTC_QUARTER  = 0xF1;
+constexpr int MIDI_POSITION_PTR = 0xF2;
+constexpr int MIDI_CLOCK        = 0xF8;
+constexpr int MIDI_START        = 0xFA;
+constexpr int MIDI_CONTINUE     = 0xFB;
+constexpr int MIDI_STOP         = 0xFC;
+constexpr int MIDI_EOX          = 0xF7; // end of sysex
+
+/* midi sync constants */
+
+constexpr int G_MIDI_SYNC_NONE    = 0;
+constexpr int G_MIDI_SYNC_CLOCK_M = 1; // master
+constexpr int G_MIDI_SYNC_CLOCK_S = 2; // slave
+constexpr int G_MIDI_SYNC_MTC_M   = 3; // master
+constexpr int G_MIDI_SYNC_MTC_S   = 4; // slave
+
+/* JSON patch keys */
+
+constexpr auto PATCH_KEY_HEADER                       = "header";
+constexpr auto PATCH_KEY_VERSION_MAJOR                = "version_major";
+constexpr auto PATCH_KEY_VERSION_MINOR                = "version_minor";
+constexpr auto PATCH_KEY_VERSION_PATCH                = "version_patch";
+constexpr auto PATCH_KEY_NAME                         = "name";
+constexpr auto PATCH_KEY_BPM                          = "bpm";
+constexpr auto PATCH_KEY_BARS                         = "bars";
+constexpr auto PATCH_KEY_BEATS                        = "beats";
+constexpr auto PATCH_KEY_QUANTIZE                     = "quantize";
+constexpr auto PATCH_KEY_MASTER_VOL_IN                = "master_vol_in";
+constexpr auto PATCH_KEY_MASTER_VOL_OUT               = "master_vol_out";
+constexpr auto PATCH_KEY_METRONOME                    = "metronome";
+constexpr auto PATCH_KEY_LAST_TAKE_ID                 = "last_take_id";
+constexpr auto PATCH_KEY_SAMPLERATE                   = "samplerate";
+constexpr auto PATCH_KEY_COLUMNS                      = "columns";
+constexpr auto PATCH_KEY_PLUGINS                      = "plugins";
+constexpr auto PATCH_KEY_MASTER_OUT_PLUGINS           = "master_out_plugins";
+constexpr auto PATCH_KEY_MASTER_IN_PLUGINS            = "master_in_plugins";
+constexpr auto PATCH_KEY_CHANNELS                     = "channels";
+constexpr auto PATCH_KEY_CHANNEL_TYPE                 = "type";
+constexpr auto PATCH_KEY_CHANNEL_ID                   = "id";
+constexpr auto PATCH_KEY_CHANNEL_SIZE                 = "size";
+constexpr auto PATCH_KEY_CHANNEL_NAME                 = "name";
+constexpr auto PATCH_KEY_CHANNEL_COLUMN               = "column";
+constexpr auto PATCH_KEY_CHANNEL_MUTE                 = "mute";
+constexpr auto PATCH_KEY_CHANNEL_SOLO                 = "solo";
+constexpr auto PATCH_KEY_CHANNEL_VOLUME               = "volume";
+constexpr auto PATCH_KEY_CHANNEL_PAN                  = "pan";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN              = "midi_in";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_VELO_AS_VOL  = "midi_in_velo_as_vol";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_KEYPRESS     = "midi_in_keypress";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_KEYREL       = "midi_in_keyrel";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_KILL         = "midi_in_kill";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_ARM          = "midi_in_arm";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_VOLUME       = "midi_in_volume";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_MUTE         = "midi_in_mute";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_FILTER       = "midi_in_filter";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_SOLO         = "midi_in_solo";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_OUT_L           = "midi_out_l";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_OUT_L_PLAYING   = "midi_out_l_playing";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_OUT_L_MUTE      = "midi_out_l_mute";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_OUT_L_SOLO      = "midi_out_l_solo";
+constexpr auto PATCH_KEY_CHANNEL_WAVE_ID              = "wave_id";
+constexpr auto PATCH_KEY_CHANNEL_KEY                  = "key";
+constexpr auto PATCH_KEY_CHANNEL_MODE                 = "mode";
+constexpr auto PATCH_KEY_CHANNEL_BEGIN                = "begin";
+constexpr auto PATCH_KEY_CHANNEL_END                  = "end";
+constexpr auto PATCH_KEY_CHANNEL_SHIFT                = "shift";
+constexpr auto PATCH_KEY_CHANNEL_HAS_ACTIONS          = "has_actions";
+constexpr auto PATCH_KEY_CHANNEL_READ_ACTIONS         = "read_actions";
+constexpr auto PATCH_KEY_CHANNEL_PITCH                = "pitch";
+constexpr auto PATCH_KEY_CHANNEL_INPUT_MONITOR        = "input_monitor";
+constexpr auto PATCH_KEY_CHANNEL_OVERDUB_PROTECTION   = "overdub_protection";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_READ_ACTIONS = "midi_in_read_actions";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_IN_PITCH        = "midi_in_pitch";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_OUT             = "midi_out";
+constexpr auto PATCH_KEY_CHANNEL_MIDI_OUT_CHAN        = "midi_out_chan";
+constexpr auto PATCH_KEY_CHANNEL_PLUGINS              = "plugins";
+constexpr auto PATCH_KEY_CHANNEL_PLUGIN_ID            = "plugin_id";
+constexpr auto PATCH_KEY_CHANNEL_ARMED                = "armed";
+constexpr auto PATCH_KEY_WAVES                        = "waves";
+constexpr auto PATCH_KEY_WAVE_ID                      = "id";
+constexpr auto PATCH_KEY_WAVE_PATH                    = "path";
+constexpr auto PATCH_KEY_ACTIONS                      = "actions";
+constexpr auto PATCH_KEY_ACTION_TYPE                  = "type";
+constexpr auto PATCH_KEY_ACTION_FRAME                 = "frame";
+constexpr auto PATCH_KEY_ACTION_F_VALUE               = "f_value";
+constexpr auto PATCH_KEY_ACTION_I_VALUE               = "i_value";
+constexpr auto PATCH_KEY_PLUGIN_ID                    = "id";
+constexpr auto PATCH_KEY_PLUGIN_PATH                  = "path";
+constexpr auto PATCH_KEY_PLUGIN_BYPASS                = "bypass";
+constexpr auto PATCH_KEY_PLUGIN_PARAMS                = "params";
+constexpr auto PATCH_KEY_PLUGIN_STATE                 = "state";
+constexpr auto PATCH_KEY_PLUGIN_MIDI_IN_PARAMS        = "midi_in_params";
+constexpr auto PATCH_KEY_COLUMN_ID                    = "id";
+constexpr auto PATCH_KEY_COLUMN_WIDTH                 = "width";
+constexpr auto PATCH_KEY_COLUMN_CHANNELS              = "channels";
+constexpr auto G_PATCH_KEY_ACTION_ID                  = "id";
+constexpr auto G_PATCH_KEY_ACTION_CHANNEL             = "channel";
+constexpr auto G_PATCH_KEY_ACTION_FRAME               = "frame";
+constexpr auto G_PATCH_KEY_ACTION_EVENT               = "event";
+constexpr auto G_PATCH_KEY_ACTION_PREV                = "prev";
+constexpr auto G_PATCH_KEY_ACTION_NEXT                = "next";
+
+/* JSON config keys */
+
+constexpr auto CONF_KEY_HEADER                        = "header";
+constexpr auto CONF_KEY_LOG_MODE                      = "log_mode";
+constexpr auto CONF_KEY_SHOW_TOOLTIPS                 = "show_tooltips";
+constexpr auto CONF_KEY_SOUND_SYSTEM                  = "sound_system";
+constexpr auto CONF_KEY_SOUND_DEVICE_IN               = "sound_device_in";
+constexpr auto CONF_KEY_SOUND_DEVICE_OUT              = "sound_device_out";
+constexpr auto CONF_KEY_CHANNELS_OUT_COUNT            = "channels_out_count";
+constexpr auto CONF_KEY_CHANNELS_OUT_START            = "channels_out_start";
+constexpr auto CONF_KEY_CHANNELS_IN_COUNT             = "channels_in_count";
+constexpr auto CONF_KEY_CHANNELS_IN_START             = "channels_in_start";
+constexpr auto CONF_KEY_SAMPLERATE                    = "samplerate";
+constexpr auto CONF_KEY_BUFFER_SIZE                   = "buffer_size";
+constexpr auto CONF_KEY_DELAY_COMPENSATION            = "delay_compensation";
+constexpr auto CONF_KEY_LIMIT_OUTPUT                  = "limit_output";
+constexpr auto CONF_KEY_RESAMPLE_QUALITY              = "resample_quality";
+constexpr auto CONF_KEY_MIDI_SYSTEM                   = "midi_system";
+constexpr auto CONF_KEY_MIDI_PORT_OUT                 = "midi_port_out";
+constexpr auto CONF_KEY_MIDI_PORT_IN                  = "midi_port_in";
+constexpr auto CONF_KEY_MIDIMAP_PATH                  = "midimap_path";
+constexpr auto CONF_KEY_LAST_MIDIMAP                  = "last_midimap";
+constexpr auto CONF_KEY_MIDI_SYNC                     = "midi_sync";
+constexpr auto CONF_KEY_MIDI_TC_FPS                   = "midi_tc_fps";
+constexpr auto CONF_KEY_MIDI_IN                       = "midi_in";
+constexpr auto CONF_KEY_MIDI_IN_FILTER                = "midi_in_filter";
+constexpr auto CONF_KEY_MIDI_IN_REWIND                = "midi_in_rewind";
+constexpr auto CONF_KEY_MIDI_IN_START_STOP            = "midi_in_start_stop";
+constexpr auto CONF_KEY_MIDI_IN_ACTION_REC            = "midi_in_action_rec";
+constexpr auto CONF_KEY_MIDI_IN_INPUT_REC             = "midi_in_input_rec";
+constexpr auto CONF_KEY_MIDI_IN_METRONOME             = "midi_in_metronome";
+constexpr auto CONF_KEY_MIDI_IN_VOLUME_IN             = "midi_in_volume_in";
+constexpr auto CONF_KEY_MIDI_IN_VOLUME_OUT            = "midi_in_volume_out";
+constexpr auto CONF_KEY_MIDI_IN_BEAT_DOUBLE           = "midi_in_beat_doble";
+constexpr auto CONF_KEY_MIDI_IN_BEAT_HALF             = "midi_in_beat_half";
+constexpr auto CONF_KEY_CHANS_STOP_ON_SEQ_HALT        = "chans_stop_on_seq_halt";
+constexpr auto CONF_KEY_TREAT_RECS_AS_LOOPS           = "treat_recs_as_loops";
+constexpr auto CONF_KEY_INPUT_MONITOR_DEFAULT_ON      = "input_monitor_default_on";
+constexpr auto CONF_KEY_OVERDUB_PROTECTION_DEFAULT_ON = "overdub_protection_default_on";
+constexpr auto CONF_KEY_PLUGINS_PATH                  = "plugins_path";
+constexpr auto CONF_KEY_PATCHES_PATH                  = "patches_path";
+constexpr auto CONF_KEY_SAMPLES_PATH                  = "samples_path";
+constexpr auto CONF_KEY_MAIN_WINDOW_X                 = "main_window_x";
+constexpr auto CONF_KEY_MAIN_WINDOW_Y                 = "main_window_y";
+constexpr auto CONF_KEY_MAIN_WINDOW_W                 = "main_window_w";
+constexpr auto CONF_KEY_MAIN_WINDOW_H                 = "main_window_h";
+constexpr auto CONF_KEY_BROWSER_X                     = "browser_x";
+constexpr auto CONF_KEY_BROWSER_Y                     = "browser_y";
+constexpr auto CONF_KEY_BROWSER_W                     = "browser_w";
+constexpr auto CONF_KEY_BROWSER_H                     = "browser_h";
+constexpr auto CONF_KEY_BROWSER_POSITION              = "browser_position";
+constexpr auto CONF_KEY_BROWSER_LAST_PATH             = "browser_last_path";
+constexpr auto CONF_KEY_BROWSER_LAST_VALUE            = "browser_last_value";
+constexpr auto CONF_KEY_ACTION_EDITOR_X               = "action_editor_x";
+constexpr auto CONF_KEY_ACTION_EDITOR_Y               = "action_editor_y";
+constexpr auto CONF_KEY_ACTION_EDITOR_W               = "action_editor_w";
+constexpr auto CONF_KEY_ACTION_EDITOR_H               = "action_editor_h";
+constexpr auto CONF_KEY_ACTION_EDITOR_ZOOM            = "action_editor_zoom";
+constexpr auto CONF_KEY_ACTION_EDITOR_SPLIT_H         = "action_editor_split_h";
+constexpr auto CONF_KEY_ACTION_EDITOR_GRID_VAL        = "action_editor_grid_val";
+constexpr auto CONF_KEY_ACTION_EDITOR_GRID_ON         = "action_editor_grid_on";
+constexpr auto CONF_KEY_SAMPLE_EDITOR_X               = "sample_editor_x";
+constexpr auto CONF_KEY_SAMPLE_EDITOR_Y               = "sample_editor_y";
+constexpr auto CONF_KEY_SAMPLE_EDITOR_W               = "sample_editor_w";
+constexpr auto CONF_KEY_SAMPLE_EDITOR_H               = "sample_editor_h";
+constexpr auto CONF_KEY_SAMPLE_EDITOR_GRID_VAL        = "sample_editor_grid_val";
+constexpr auto CONF_KEY_SAMPLE_EDITOR_GRID_ON         = "sample_editor_grid_on";
+constexpr auto CONF_KEY_ACTION_EDITOR_PIANO_ROLL_Y    = "piano_roll_y";
+constexpr auto CONF_KEY_PLUGIN_LIST_X                 = "plugin_list_x";
+constexpr auto CONF_KEY_PLUGIN_LIST_Y                 = "plugin_list_y";
+constexpr auto CONF_KEY_PLUGIN_CHOOSER_X              = "plugin_chooser_x";
+constexpr auto CONF_KEY_PLUGIN_CHOOSER_Y              = "plugin_chooser_y";
+constexpr auto CONF_KEY_PLUGIN_CHOOSER_W              = "plugin_chooser_w";
+constexpr auto CONF_KEY_PLUGIN_CHOOSER_H              = "plugin_chooser_h";
+constexpr auto CONF_KEY_MIDI_INPUT_X                  = "midi_input_x";
+constexpr auto CONF_KEY_MIDI_INPUT_Y                  = "midi_input_y";
+constexpr auto CONF_KEY_MIDI_INPUT_W                  = "midi_input_w";
+constexpr auto CONF_KEY_MIDI_INPUT_H                  = "midi_input_h";
+constexpr auto CONF_KEY_PLUGIN_SORT_METHOD            = "plugin_sort_method";
+constexpr auto CONF_KEY_REC_TRIGGER_MODE              = "rec_trigger_mode";
+constexpr auto CONF_KEY_REC_TRIGGER_LEVEL             = "rec_trigger_level";
+constexpr auto CONF_KEY_INPUT_REC_MODE                = "input_rec_mode";
+constexpr auto CONF_KEY_BIND_PLAY                     = "key_bind_play";
+constexpr auto CONF_KEY_BIND_REWIND                   = "key_bind_rewind";
+constexpr auto CONF_KEY_BIND_RECORD_ACTIONS           = "key_bind_record_actions";
+constexpr auto CONF_KEY_BIND_RECORD_INPUT             = "key_bind_record_input";
+constexpr auto CONF_KEY_BIND_EXIT                     = "key_bind_record_exit";
+
+/* JSON midimaps keys */
+
+constexpr auto MIDIMAP_KEY_BRAND             = "brand";
+constexpr auto MIDIMAP_KEY_DEVICE            = "device";
+constexpr auto MIDIMAP_KEY_INIT_COMMANDS     = "init_commands";
+constexpr auto MIDIMAP_KEY_MUTE_ON           = "mute_on";
+constexpr auto MIDIMAP_KEY_MUTE_OFF          = "mute_off";
+constexpr auto MIDIMAP_KEY_SOLO_ON           = "solo_on";
+constexpr auto MIDIMAP_KEY_SOLO_OFF          = "solo_off";
+constexpr auto MIDIMAP_KEY_WAITING           = "waiting";
+constexpr auto MIDIMAP_KEY_PLAYING           = "playing";
+constexpr auto MIDIMAP_KEY_PLAYING_INAUDIBLE = "playing_inaudible";
+constexpr auto MIDIMAP_KEY_STOPPING          = "stopping";
+constexpr auto MIDIMAP_KEY_STOPPED           = "stopped";
+constexpr auto MIDIMAP_KEY_CHANNEL           = "channel";
+constexpr auto MIDIMAP_KEY_MESSAGE           = "message";
+
+#endif
diff --git a/src/core/engine.cpp b/src/core/engine.cpp
new file mode 100644 (file)
index 0000000..2b05d8f
--- /dev/null
@@ -0,0 +1,425 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/engine.h"
+#include "core/model/model.h"
+#include "core/model/storage.h"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include <memory>
+
+namespace giada::m
+{
+bool LoadState::isGood() const
+{
+       return patch == G_PATCH_OK && missingWaves.empty() && missingPlugins.empty();
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Engine::Engine()
+: midiMapper(kernelMidi)
+, channelManager(conf.data, model)
+, midiDispatcher(model)
+, actionRecorder(model)
+, synchronizer(conf.data, kernelMidi)
+, sequencer(model, synchronizer, jackTransport)
+, mixer(model)
+, mixerHandler(model, mixer)
+, recorder(model, sequencer, mixerHandler)
+#ifdef WITH_VST
+, pluginHost(model)
+#endif
+{
+       kernelAudio.onAudioCallback = [this](KernelAudio::CallbackInfo info) {
+               return audioCallback(info);
+       };
+
+       kernelMidi.onMidiReceived = [this](uint32_t msg) { midiDispatcher.dispatch(msg); };
+
+#ifdef WITH_AUDIO_JACK
+       synchronizer.onJackRewind = [this]() {
+               eventDispatcher.pumpMidiEvent({EventDispatcher::EventType::SEQUENCER_REWIND_JACK});
+       };
+       synchronizer.onJackChangeBpm = [this](float bpm) {
+               eventDispatcher.pumpMidiEvent({EventDispatcher::EventType::SEQUENCER_BPM_JACK, 0, 0, bpm});
+       };
+       synchronizer.onJackStart = [this]() {
+               eventDispatcher.pumpMidiEvent({EventDispatcher::EventType::SEQUENCER_START_JACK});
+       };
+       synchronizer.onJackStop = [this]() {
+               eventDispatcher.pumpMidiEvent({EventDispatcher::EventType::SEQUENCER_STOP_JACK});
+       };
+#endif
+
+       eventDispatcher.onMidiLearn       = [this](const MidiEvent& e) { midiDispatcher.learn(e); };
+       eventDispatcher.onMidiProcess     = [this](const MidiEvent& e) { midiDispatcher.process(e); };
+       eventDispatcher.onProcessChannels = [this](const EventDispatcher::EventBuffer& eb) {
+               for (Channel& ch : model.get().channels)
+                       ch.react(eb);
+               model.swap(model::SwapType::SOFT);
+       };
+       eventDispatcher.onProcessSequencer = [this](const EventDispatcher::EventBuffer& eb) {
+               sequencer.react(eb, kernelAudio.getSampleRate());
+       };
+       eventDispatcher.onMixerSignalCallback = [this]() {
+               recorder.startInputRecOnCallback();
+       };
+       eventDispatcher.onMixerEndOfRecCallback = [this]() {
+               if (recorder.isRecordingInput())
+                       recorder.stopInputRec(conf.data.inputRecMode, kernelAudio.getSampleRate());
+       };
+
+       midiDispatcher.onDispatch = [this](EventDispatcher::EventType event, Action action) {
+               /* Notify Event Dispatcher when a MIDI signal is received. */
+               eventDispatcher.pumpMidiEvent({event, 0, 0, action});
+       };
+
+       midiDispatcher.onEventReceived = [this]() {
+               recorder.startActionRecOnCallback();
+       };
+
+       mixer.onSignalTresholdReached = [this]() {
+               /* Invokes the signal callback. This is done by pumping a MIXER_SIGNAL_CALLBACK
+        event to the Event Dispatcher, rather than invoking the callback directly.
+        This is done on purpose: the callback might (and surely will) contain 
+        blocking stuff from model:: that the realtime thread cannot perform directly. */
+               eventDispatcher.pumpUIevent({EventDispatcher::EventType::MIXER_SIGNAL_CALLBACK});
+       };
+
+       mixer.onEndOfRecording = [this]() {
+               /* Same rationale as above, for the end-of-recording callback. */
+               eventDispatcher.pumpUIevent({EventDispatcher::EventType::MIXER_END_OF_REC_CALLBACK});
+       };
+
+       mixerHandler.onChannelsAltered = [this]() {
+               if (!recorder.canEnableFreeInputRec())
+                       conf.data.inputRecMode = InputRecMode::RIGID;
+       };
+       mixerHandler.onChannelRecorded = [this](Frame recordedFrames) {
+               std::string filename = "TAKE-" + std::to_string(patch.data.lastTakeId++) + ".wav";
+               return waveManager.createEmpty(recordedFrames, G_MAX_IO_CHANS, kernelAudio.getSampleRate(), filename);
+       };
+
+       sequencer.onAboutStart = [this](SeqStatus status) {
+               /* TODO move this logic to Recorder */
+               if (status == SeqStatus::WAITING)
+                       recorder.stopActionRec(actionRecorder);
+               conf.data.recTriggerMode = RecTriggerMode::NORMAL;
+       };
+
+       sequencer.onAboutStop = [this]() {
+               /* If recordings (both input and action) are active deactivate them, but 
+       store the takes. RecManager takes care of it. */
+               /* TODO move this logic to Recorder */
+               if (recorder.isRecordingAction())
+                       recorder.stopActionRec(actionRecorder);
+               else if (recorder.isRecordingInput())
+                       recorder.stopInputRec(conf.data.inputRecMode, kernelAudio.getSampleRate());
+       };
+
+       sequencer.onBpmChange = [this](float oldVal, float newVal, int quantizerStep) {
+               actionRecorder.updateBpm(oldVal / newVal, quantizerStep);
+       };
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Engine::updateMixerModel()
+{
+       model.get().mixer.limitOutput     = conf.data.limitOutput;
+       model.get().mixer.allowsOverdub   = conf.data.inputRecMode == InputRecMode::RIGID;
+       model.get().mixer.maxFramesToRec  = conf.data.inputRecMode == InputRecMode::FREE ? sequencer.getMaxFramesInLoop(kernelAudio.getSampleRate()) : sequencer.getFramesInLoop();
+       model.get().mixer.recTriggerLevel = conf.data.recTriggerLevel;
+       model.swap(model::SwapType::NONE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Engine::init()
+{
+       if (!conf.read())
+               u::log::print("[Engine::init] Can't read configuration file! Using default values\n");
+
+       model::load(conf.data);
+
+       if (!u::log::init(conf.data.logMode))
+               u::log::print("[Engine::init] log init failed! Using default stdout\n");
+
+       init::printBuildInfo();
+
+       midiMapper.init();
+       if (midiMapper.read(conf.data.midiMapPath) != MIDIMAP_READ_OK)
+               u::log::print("[Engine::init] MIDI map read failed!\n");
+
+       /* Initialize KernelAudio. If fails, interrupt the Engine initialization:
+    Giada can't work without a functional KernelAudio. */
+
+       kernelAudio.openDevice(conf.data);
+       if (!kernelAudio.isReady())
+               return;
+
+#ifdef WITH_AUDIO_JACK
+       if (kernelAudio.getAPI() == G_SYS_API_JACK)
+               jackTransport.setHandle(kernelAudio.getJackHandle());
+#endif
+
+       mixerHandler.reset(sequencer.getMaxFramesInLoop(kernelAudio.getSampleRate()),
+           kernelAudio.getBufferSize(), channelManager);
+       sequencer.reset(kernelAudio.getSampleRate());
+#ifdef WITH_VST
+       pluginHost.reset(kernelAudio.getBufferSize());
+       pluginManager.reset(static_cast<PluginManager::SortMethod>(conf.data.pluginSortMethod));
+#endif
+       mixer.enable();
+       kernelAudio.startStream();
+
+       kernelMidi.openOutDevice(conf.data.midiSystem, conf.data.midiPortOut);
+       kernelMidi.openInDevice(conf.data.midiSystem, conf.data.midiPortIn);
+       kernelMidi.logPorts();
+
+       midiMapper.sendInitMessages(midiMapper.currentMap);
+
+       eventDispatcher.start();
+
+       updateMixerModel();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Engine::reset()
+{
+       /* Managers first, due to the internal ID numbering. */
+
+       channelManager.reset();
+       waveManager.reset();
+#ifdef WITH_VST
+       pluginManager.reset(static_cast<PluginManager::SortMethod>(conf.data.pluginSortMethod));
+#endif
+
+       /* Then all other components. */
+
+       model.reset();
+       mixerHandler.reset(sequencer.getMaxFramesInLoop(kernelAudio.getSampleRate()),
+           kernelAudio.getBufferSize(), channelManager);
+       synchronizer.reset();
+       sequencer.reset(kernelAudio.getSampleRate());
+       actionRecorder.reset();
+#ifdef WITH_VST
+       pluginHost.reset(kernelAudio.getBufferSize());
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Engine::shutdown()
+{
+       if (kernelAudio.isReady())
+       {
+               kernelAudio.closeDevice();
+               u::log::print("[Engine::shutdown] KernelAudio closed\n");
+               mixer.disable();
+               u::log::print("[Engine::shutdown] Mixer closed\n");
+       }
+
+       model::store(conf.data);
+       if (!conf.write())
+               u::log::print("[Engine::shutdown] error while saving configuration file!\n");
+       else
+               u::log::print("[Engine::shutdown] configuration saved\n");
+
+       u::log::close();
+
+#ifdef WITH_VST
+       /* Currently the Engine is global/static, and so are all of its sub-components, 
+       Model included. Some plug-ins (JUCE-based ones) crash hard on destructor when 
+       deleted as a result of returning from main, so it's better to free them all first.
+       TODO - investigate this! */
+
+       pluginHost.freeAllPlugins();
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+int Engine::audioCallback(KernelAudio::CallbackInfo kernelInfo)
+{
+       mcl::AudioBuffer out(static_cast<float*>(kernelInfo.outBuf), kernelInfo.bufferSize, kernelInfo.channelsOutCount);
+       mcl::AudioBuffer in;
+       if (kernelInfo.channelsInCount > 0)
+               in = mcl::AudioBuffer(static_cast<float*>(kernelInfo.inBuf), kernelInfo.bufferSize, kernelInfo.channelsInCount);
+
+       /* Clean up output buffer before any rendering. Do this even if mixer is
+       disabled to avoid audio leftovers during a temporary suspension (e.g. when
+       loading a new patch). */
+
+       out.clear();
+
+       if (!kernelInfo.ready)
+               return 0;
+
+       /* Prepare the LayoutLock. From this point on (until out of scope) the 
+       Layout is locked for realtime rendering by the audio thread. Rendering 
+       functions must access the realtime layout coming from layoutLock.get(). */
+
+       const model::LayoutLock layoutLock = model.get_RT();
+       const model::Layout&    layout_RT  = layoutLock.get();
+
+       /* Mixer disabled, nothing to do here. */
+
+       if (!layout_RT.mixer.a_isActive())
+               return 0;
+
+#ifdef WITH_AUDIO_JACK
+       if (kernelInfo.withJack)
+               synchronizer.recvJackSync(jackTransport.getState());
+#endif
+
+       /* If the sequencer is running, advance it first (i.e. parse it for events). 
+       Also advance channels (i.e. let them react to sequencer events), only if the 
+       layout is not locked: another thread might altering channel's data in the 
+       meantime (e.g. Plugins or Waves). */
+
+       if (layout_RT.sequencer.isRunning())
+       {
+               const Frame        currentFrame  = sequencer.getCurrentFrame();
+               const Frame        bufferSize    = in.countFrames();
+               const Frame        quantizerStep = sequencer.getQuantizerStep();              // TODO pass this to sequencer.advance - or better, Advancer class
+               const Range<Frame> renderRange   = {currentFrame, currentFrame + bufferSize}; // TODO pass this to sequencer.advance - or better, Advancer class
+
+               const Sequencer::EventBuffer& events = sequencer.advance(bufferSize, actionRecorder);
+               sequencer.render(out);
+               if (!layout_RT.locked)
+                       mixer.advanceChannels(events, layout_RT, renderRange, quantizerStep);
+       }
+
+       /* Then render Mixer: render channels, process I/O. */
+
+       mixer.render(out, in, layout_RT);
+
+       return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Engine::store(const std::string& projectName, const std::string& projectPath,
+    const std::string& patchPath, std::function<void(float)> progress)
+{
+       progress(0.0f);
+
+       if (!u::fs::mkdir(projectPath))
+       {
+               u::log::print("[Engine::store] Unable to make project directory!\n");
+               return false;
+       }
+
+       u::log::print("[Engine::store] Project dir created: %s\n", projectPath);
+
+       /* Update all existing file paths in Waves, so that they point to the project
+       folder they belong to. */
+
+       for (std::unique_ptr<Wave>& w : model.getAllShared<model::WavePtrs>())
+       {
+               w->setPath(makeUniqueWavePath(projectPath, *w, model.getAllShared<model::WavePtrs>()));
+               waveManager.save(*w, w->getPath()); // TODO - error checking
+       }
+
+       progress(0.3f);
+
+       /* Write Model into Patch, then into file. */
+
+       patch.data.name = projectName;
+       model::store(patch.data);
+
+       progress(0.6f);
+
+       if (!patch.write(patchPath))
+               return false;
+
+       /* Store the parent folder the project belongs to, in order to reuse it the 
+       next time. */
+
+       conf.data.patchPath = u::fs::getUpDir(u::fs::getUpDir(patchPath));
+
+       u::log::print("[Engine::store] Project patch saved as %s\n", patchPath);
+
+       progress(1.0f);
+
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+LoadState Engine::load(const std::string& projectPath, const std::string& patchPath,
+    std::function<void(float)> progress)
+{
+       u::log::print("[Engine::load] Load project from %s\n", projectPath);
+
+       progress(0.0f);
+
+       patch.reset();
+       if (int res = patch.read(patchPath, projectPath); res != G_PATCH_OK)
+               return {res};
+
+       progress(0.3f);
+
+       /* Then suspend Mixer, reset and fill the model. */
+
+       mixer.disable();
+       reset();
+       LoadState state = m::model::load(patch.data);
+
+       progress(0.6f);
+
+       /* Prepare the engine. Recorder has to recompute the actions positions if 
+       the current samplerate != patch samplerate. Clock needs to update frames
+       in sequencer. */
+
+       mixerHandler.updateSoloCount();
+       actionRecorder.updateSamplerate(kernelAudio.getSampleRate(), patch.data.samplerate);
+       sequencer.recomputeFrames(kernelAudio.getSampleRate());
+       mixer.allocRecBuffer(sequencer.getMaxFramesInLoop(kernelAudio.getSampleRate()));
+
+       progress(0.9f);
+
+       /* Store the parent folder the project belongs to, in order to reuse it the 
+       next time. */
+
+       conf.data.patchPath = u::fs::getUpDir(projectPath);
+
+       /* Mixer is ready to go back online. */
+
+       mixer.enable();
+
+       progress(1.0f);
+
+       state.patch = G_PATCH_OK;
+       return state;
+}
+
+} // namespace giada::m
diff --git a/src/core/engine.h b/src/core/engine.h
new file mode 100644 (file)
index 0000000..c7d242e
--- /dev/null
@@ -0,0 +1,136 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_ENGINE_H
+#define G_ENGINE_H
+
+#include "core/actions/actionRecorder.h"
+#include "core/actions/actions.h"
+#include "core/channels/channelManager.h"
+#include "core/conf.h"
+#include "core/eventDispatcher.h"
+#include "core/init.h"
+#include "core/jackTransport.h"
+#include "core/kernelAudio.h"
+#include "core/kernelMidi.h"
+#include "core/midiDispatcher.h"
+#include "core/midiMapper.h"
+#include "core/mixer.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/patch.h"
+#include "core/plugins/pluginHost.h"
+#include "core/plugins/pluginManager.h"
+#include "core/recorder.h"
+#include "core/sequencer.h"
+#include "core/synchronizer.h"
+#include "core/waveManager.h"
+
+namespace giada::m
+{
+struct LoadState
+{
+       bool isGood() const;
+
+       int                      patch          = G_PATCH_OK;
+       std::vector<std::string> missingWaves   = {};
+       std::vector<std::string> missingPlugins = {};
+};
+
+class Engine final
+{
+public:
+       /* Engine()
+    Prepares all sub-components by constructing them and setting up the required
+    callback for inter-component communication. It doesn't start the engine yet. */
+
+       Engine();
+
+       /* store
+       Saves the current state to a Patch, then saves it to file. Returns true
+       on success. */
+
+       bool store(const std::string& projectName, const std::string& projectPath,
+           const std::string& patchPath, std::function<void(float)> progress);
+
+       /* load
+       Reads a Patch from file and then de-serialize its content into the model. 
+       Returns a LoadState object. */
+
+       LoadState load(const std::string& projectPath, const std::string& patchPath,
+           std::function<void(float)> progress);
+
+       /* updateMixerModel
+       Updates some values in model::Mixer data struct needed by m::Mixer for the
+       audio rendering. Call this whenever the audio configuration changes. */
+
+       void updateMixerModel();
+
+       /* init
+    Initializes all sub-components. If KernelAudio fails to start, the process
+    interrupts and Giada is put in an invalid state. */
+
+       void init();
+
+       /* reset
+    Resets all sub-components to the initial state. Useful when Giada needs to
+    be brought back to the starup state. */
+
+       void reset();
+
+       /* shutdown
+    Closes the current audio device. */
+
+       void shutdown();
+
+       model::Model           model;
+       Conf                   conf;
+       Patch                  patch;
+       KernelAudio            kernelAudio;
+       KernelMidi             kernelMidi;
+       JackTransport          jackTransport;
+       WaveManager            waveManager;
+       EventDispatcher        eventDispatcher;
+       MidiMapper<KernelMidi> midiMapper;
+       ChannelManager         channelManager;
+       MidiDispatcher         midiDispatcher;
+       ActionRecorder         actionRecorder;
+       Synchronizer           synchronizer;
+       Sequencer              sequencer;
+       Mixer                  mixer;
+       MixerHandler           mixerHandler;
+       Recorder               recorder;
+#ifdef WITH_VST
+       PluginHost    pluginHost;
+       PluginManager pluginManager;
+#endif
+
+private:
+       int audioCallback(KernelAudio::CallbackInfo);
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/eventDispatcher.cpp b/src/core/eventDispatcher.cpp
new file mode 100644 (file)
index 0000000..e0c8939
--- /dev/null
@@ -0,0 +1,112 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/eventDispatcher.h"
+#include "core/const.h"
+#include <cassert>
+
+namespace giada::m
+{
+EventDispatcher::EventDispatcher()
+: onMidiLearn(nullptr)
+, onMidiProcess(nullptr)
+, onProcessChannels(nullptr)
+, onProcessSequencer(nullptr)
+, onMixerSignalCallback(nullptr)
+, onMixerEndOfRecCallback(nullptr)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void EventDispatcher::start()
+{
+       m_worker.start([this]() { process(); }, /*sleep=*/G_EVENT_DISPATCHER_RATE_MS);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void EventDispatcher::pumpUIevent(Event e) { UIevents.push(e); }
+void EventDispatcher::pumpMidiEvent(Event e) { MidiEvents.push(e); }
+
+/* -------------------------------------------------------------------------- */
+
+void EventDispatcher::processFuntions()
+{
+       assert(onMidiLearn != nullptr);
+       assert(onMidiProcess != nullptr);
+       assert(onMixerSignalCallback != nullptr);
+       assert(onMixerEndOfRecCallback != nullptr);
+
+       for (const Event& e : m_eventBuffer)
+       {
+               switch (e.type)
+               {
+               case EventType::MIDI_DISPATCHER_LEARN:
+                       onMidiLearn(std::get<Action>(e.data).event);
+                       break;
+
+               case EventType::MIDI_DISPATCHER_PROCESS:
+                       onMidiProcess(std::get<Action>(e.data).event);
+                       break;
+
+               case EventType::MIXER_SIGNAL_CALLBACK:
+                       onMixerSignalCallback();
+                       break;
+
+               case EventType::MIXER_END_OF_REC_CALLBACK:
+                       onMixerEndOfRecCallback();
+                       break;
+
+               default:
+                       break;
+               }
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void EventDispatcher::process()
+{
+       assert(onProcessChannels != nullptr);
+       assert(onProcessSequencer != nullptr);
+
+       m_eventBuffer.clear();
+
+       Event e;
+       while (UIevents.pop(e))
+               m_eventBuffer.push_back(e);
+       while (MidiEvents.pop(e))
+               m_eventBuffer.push_back(e);
+
+       if (m_eventBuffer.size() == 0)
+               return;
+
+       processFuntions();
+       onProcessChannels(m_eventBuffer);
+       onProcessSequencer(m_eventBuffer);
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/eventDispatcher.h b/src/core/eventDispatcher.h
new file mode 100644 (file)
index 0000000..3462201
--- /dev/null
@@ -0,0 +1,143 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_EVENT_DISPATCHER_H
+#define G_EVENT_DISPATCHER_H
+
+#include "core/const.h"
+#include "core/queue.h"
+#include "core/ringBuffer.h"
+#include "core/types.h"
+#include "core/worker.h"
+#include "src/core/actions/action.h"
+#include <atomic>
+#include <functional>
+#include <thread>
+#include <variant>
+
+/* giada::m::EventDispatcher
+Takes events from the two queues (MIDI and UI) filled by c::events and turns 
+them into actual changes in the data model. The EventDispatcher runs in a
+separate worker thread. */
+
+namespace giada::m
+{
+class EventDispatcher
+{
+public:
+       enum class EventType
+       {
+               KEY_PRESS,
+               KEY_RELEASE,
+               KEY_KILL,
+               SEQUENCER_START,
+               SEQUENCER_STOP,
+               SEQUENCER_REWIND,
+#ifdef WITH_AUDIO_JACK
+               SEQUENCER_START_JACK,
+               SEQUENCER_STOP_JACK,
+               SEQUENCER_REWIND_JACK,
+               SEQUENCER_BPM_JACK,
+#endif
+               MIDI,
+               MIDI_DISPATCHER_LEARN,
+               MIDI_DISPATCHER_PROCESS,
+               MIXER_SIGNAL_CALLBACK,
+               MIXER_END_OF_REC_CALLBACK,
+               CHANNEL_TOGGLE_READ_ACTIONS,
+               CHANNEL_KILL_READ_ACTIONS,
+               CHANNEL_TOGGLE_ARM,
+               CHANNEL_MUTE,
+               CHANNEL_SOLO,
+               CHANNEL_VOLUME,
+               CHANNEL_PITCH,
+               CHANNEL_PAN
+       };
+
+       struct Event
+       {
+               using EventData = std::variant<int, float, Action>;
+
+               EventType type;
+               Frame     delta     = 0;
+               ID        channelId = 0;
+               EventData data      = {};
+       };
+
+       /* EventBuffer
+       Alias for a RingBuffer containing events to be sent to engine. The double 
+       size is due to the presence of two distinct Queues for collecting events 
+       coming from other threads. See below. */
+
+       using EventBuffer = RingBuffer<Event, G_MAX_DISPATCHER_EVENTS * 2>;
+
+       EventDispatcher();
+
+       /* start
+       Starts the internal worker on a separate thread. Call this on startup. */
+
+       void start();
+
+       void pumpUIevent(Event e);
+       void pumpMidiEvent(Event e);
+
+       /* Event queues
+       Collect events coming from the UI or MIDI devices. Our poor man's Queue is a 
+       single-producer/single-consumer one, so we need two queues for two writers. 
+       TODO - let's add a multi-producer queue sooner or later! */
+       /*TODO - make them private*/
+
+       Queue<Event, G_MAX_DISPATCHER_EVENTS> UIevents;
+       Queue<Event, G_MAX_DISPATCHER_EVENTS> MidiEvents;
+
+       /* on[...]
+       Callbacks fired when something happens in the Event Dispatcher. */
+
+       std::function<void(const MidiEvent& e)> onMidiLearn;
+       std::function<void(const MidiEvent& e)> onMidiProcess;
+       std::function<void(const EventBuffer&)> onProcessChannels;
+       std::function<void(const EventBuffer&)> onProcessSequencer;
+       std::function<void()>                   onMixerSignalCallback;
+       std::function<void()>                   onMixerEndOfRecCallback;
+
+private:
+       void processFuntions();
+       void process();
+
+       /* m_worker
+       A separate thread responsible for the event processing. */
+
+       Worker m_worker;
+
+       /* m_eventBuffer
+       Buffer of events sent to channels for event parsing. This is filled with 
+       Events coming from the two event queues.*/
+
+       EventBuffer m_eventBuffer;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/graphics.cpp b/src/core/graphics.cpp
new file mode 100644 (file)
index 0000000..10eb1b0
--- /dev/null
@@ -0,0 +1,1926 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "graphics.h"
+
+const char* giada_logo_xpm[] = {
+    "245 86 12 1",
+    "  c #191919",
+    ". c #303030",
+    "+ c #404040",
+    "@ c #454545",
+    "# c #5A5A5A",
+    "$ c #686868",
+    "% c #818181",
+    "& c #9C9C9C",
+    "* c #B8B8B8",
+    "= c #CDCDCD",
+    "- c #DDDDDD",
+    "; c #FEFEFE",
+    "                                   ...+++@@@@++...                                                                                                                                                                                                   ",
+    "                               .+@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                               ",
+    "                            .+@@@@@@@@@@@@@@@@@@@@@@@@@@+.                                                                                                                                                                                           ",
+    "                         .+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                         ",
+    "                       .+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                       ",
+    "                      @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                     ",
+    "                    +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                   ",
+    "                  .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+                                                                                                                                                                                  ",
+    "                 .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                ",
+    "                @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                               ",
+    "               @@@@@@@@@@@@@@@@@@++$*--;;;;;;;;-=&@+@@@@@@@@@@@@@@@@@@+                                                                                                                                                                              ",
+    "             .@@@@@@@@@@@@@@@@+#&=;;;;;;;;;;;;;;;;;-*%++@@@@@@@@@@@@@@@+                                                                                                                                                                             ",
+    "            .@@@@@@@@@@@@@@@@$=;;;;;;;;;;;;;;;;;;;;;;;;&#+@@@@@@@@@@@@@@@                                                                                                                                                                            ",
+    "           .@@@@@@@@@@@@@@@$;;;;;;;;;;;;;;;;;;;;;;;;;;;;;*@@@@@@@@@@@@@@@@                                                                                                                                                                           ",
+    "           @@@@@@@@@@@@@@#-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;*+@@@@@@@@@@@@@+                                                                                                                                                                          ",
+    "          @@@@@@@@@@@@@+*;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;$+@@@@@@@@@@@@+                                                                                                                                                                         ",
+    "         @@@@@@@@@@@@@#-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;&@@@@@@@@@@@@@.                                                                                                                                                                        ",
+    "        .@@@@@@@@@@@@$;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-+@@@@@@@@@@@@                                                                                                                                                                        ",
+    "       .@@@@@@@@@@@+%;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-@@@@@@@@@@@@@                                                                                                                                                                       ",
+    "       @@@@@@@@@@@+&;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;#@@@@@@@@@@@.                                                                                                                                                                      ",
+    "      +@@@@@@@@@@@%;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;@@@@@@@@@@@@                                                                                                                                                                      ",
+    "      @@@@@@@@@@@$;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-+@@@@@@@@@@+                                                                                                                                                                     ",
+    "     @@@@@@@@@@@#;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;=@@@@@@@@@@@.                                                                                                                                                                    ",
+    "    .@@@@@@@@@@+-;;;;;;;;;;;;;;;;;;;;;=*&%%%%&*-;;;;;;;;;;;;;;;;;;;;;%+@@@@@@@@@@                                                                                                                                                                    ",
+    "    +@@@@@@@@@@*;;;;;;;;;;;;;;;;;;;-%++@@@@@@@++@*-;;;;;;;;;;;;;;;;;;;#@@@@@@@@@@.                                                                                                                                                                   ",
+    "   .@@@@@@@@@@#;;;;;;;;;;;;;;;;;;=@@@@@@@@@@@@@@@@@$;;;;;;;;;;;;;;;;;;=+@@@@@@@@@@                                                                                                                                                                   ",
+    "   +@@@@@@@@@@-;;;;;;;;;;;;;;;;-$+@@@@@@@@@@@@@@@@@@@&;;;;;;;;;;;;;;;;;%@@@@@@@@@@.                        %*------=*%.              $%%%%%%%                $%%%%%%%%.               #%%%%%%%%%%%%%%$@                         %%%%%%%%%            ",
+    "   @@@@@@@@@@%;;;;;;;;;;;;;;;;*@@@@@@@@@@@@@@@@@@@@@@+$;;;;;;;;;;;;;;;;-+@@@@@@@@@+                     #=;;;;;;;;;;;;;=$           .;;;;;;;;&              &;;;;;;;;;;               ;;;;;;;;;;;;;;;;;;-&#                    -;;;;;;;;;=           ",
+    "  .@@@@@@@@@@;;;;;;;;;;;;;;;;*+@@@@@@@@@@@@@@@@@@@@@@@@@;;;;;;;;;;;;;;;;%@@@@@@@@@@                   .=;;;;;;;;;;;;;;;;;;#         +;;;;;;;;*              ;;;;;;;;;;;&             .;;;;;;;;;;;;;;;;;;;;;;@                 .;;;;;;;;;;;.          ",
+    "  +@@@@@@@@@%;;;;;;;;;;;;;;;*@@@@@@@@@@@@@@@@@@@@@@@@@@@@;;;;;;;;;;;;;;;;@@@@@@@@@@.                 @;;;;;;;;;;;;;;;;;;;;;*        +;;;;;;;;*             +;;;;;;;;;;;;             .;;;;;;;;;;;;;;;;;;;;;;;*                &;;;;;;;;;;;*          ",
+    "  @@@@@@@@@+-;;;;;;;;;;;;;;*+@@@@@@@@@@@@@@@@@@@@@@@@@@@@#;;;;;;;;;;;;;;;$@@@@@@@@@+                @;;;;;;;;;;;;;;;;;;;;;;;=       +;;;;;;;;*             *;;;;;;;;;;;;.            .;;;;;;;;;;;;;;;;;;;;;;;;=               ;;;;;;;;;;;;;          ",
+    " .@@@@@@@@@#;;;;;;;;;;;;;;-@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%;;;;;;;;;;;;;;=+@@@@@@@@@               .;;;;;;;;;;;;;;;;;;;;;;;;;&      +;;;;;;;;*            .;;;;;;;;;;;;;&            .;;;;;;;;;;;;;;;;;;;;;;;;;*             #;;;;;;;;;;;;;+         ",
+    " +@@@@@@@@@&;;;;;;;;;;;;;;#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+*;;;;;;;;;;;;;;#@@@@@@@@@               *;;;;;;;;;;;;;;;;;;;;;;;;;;#     +;;;;;;;;*            $;;;;;;;;;;;;;;            .;;;;;;;;;;;;;;;;;;;;;;;;;;#            *;;;;;;;;;;;;;=         ",
+    " @@@@@@@@@+-;;;;;;;;;;;;;*@@@@@@@@@@@@+#*;;;-&@@@@@@@@@@@@@@;;;;;;;;;;;;;;%@@@@@@@@@.             +;;;;;;;;;;-#   #-;;;;;;;;;;=     +;;;;;;;;*            =;;;;;;;;;;;;;;#           .;;;;;;;;-&&&&*-;;;;;;;;;;;;            ;;;;;;;;;;;;;;;.        ",
+    " @@@@@@@@@+;;;;;;;;;;;;;;@@@@@@@@@@@+%-;;;;;;;;*+@@@@@@@@@@+&;;;;;;;;;;;;;*+@@@@@@@@+             &;;;;;;;;;%       %;;;;;;;;;;     +;;;;;;;;*           .;;;;;;;;;;;;;;;*           .;;;;;;;;=       #;;;;;;;;;;#          %;;;;;;;;;;;;;;;$        ",
+    ".@@@@@@@@@%;;;;;;;;;;;;;=@@@@@@@@@@+&;;;;;;;;;;;;#@@@@@@@@@@#;;;;;;;;;;;;;;+@@@@@@@@@             =;;;;;;;;=         =;;;;;;;;;@    +;;;;;;;;*           &;;;;;;;%;;;;;;;;           .;;;;;;;;=         ;;;;;;;;;&          -;;;;;;;%;;;;;;;=        ",
+    ".@@@@@@@@@=;;;;;;;;;;;;;$@@@@@@@@@+*;;;;;;;;;;;;;;#@@@@@@@@@@-;;;;;;;;;;;;;#@@@@@@@@@             ;;;;;;;;;@          -;;;;;;;;.    +;;;;;;;;*           -;;;;;;;+*;;;;;;;%          .;;;;;;;;=         %;;;;;;;;-          ;;;;;;;; ;;;;;;;;.       ",
+    ".@@@@@@@@@-;;;;;;;;;;;;;+@@@@@@@@@%;;;;;;;;;;;;;;;;+@@@@@@@@@%;;;;;;;;;;;;;%@@@@@@@@@             ;;;;;;;;;                         +;;;;;;;;*          .;;;;;;;; #;;;;;;;=          .;;;;;;;;=          ;;;;;;;;;         &;;;;;;;& &;;;;;;;&       ",
+    "+@@@@@@@@@;;;;;;;;;;;;;*+@@@@@@@@@;;;;;;;;;;;;;;;;;&@@@@@@@@@@;;;;;;;;;;;;;&@@@@@@@@@            .;;;;;;;;-                         +;;;;;;;;*          *;;;;;;;%  ;;;;;;;;          .;;;;;;;;=          -;;;;;;;;         ;;;;;;;;. @;;;;;;;-       ",
+    "+@@@@@@@@@;;;;;;;;;;;;;&@@@@@@@@@%;;;;;;;;;;;;;;;;;-+@@@@@@@@+-;;;;;;;;;;;;*@@@@@@@@@.           .;;;;;;;;-                         +;;;;;;;;*          ;;;;;;;;.  *;;;;;;;&         .;;;;;;;;=          =;;;;;;;;.       .;;;;;;;;   ;;;;;;;;.      ",
+    "+@@@@@@@@@;;;;;;;;;;;;;%@@@@@@@@+-;;;;;;;;;;;;;;;;;;$@@@@@@@@@-;;;;;;;;;;;;=@@@@@@@@@.           .;;;;;;;;-      .%%%%%%%%%%%%$     +;;;;;;;;*         +;;;;;;;-   .;;;;;;;;         .;;;;;;;;=          =;;;;;;;;.       &;;;;;;;*   %;;;;;;;*      ",
+    "@@@@@@@@@@;;;;;;;;;;;;;$@@@@@@@@@;;;;;;;;;;;;;;;;;;;&@@@@@@@@@=;;;;;;;;;;;;=+@@@@@@@@.           +;;;;;;;;-      -;;;;;;;;;;;;;%    +;;;;;;;;*         *;;;;;;;&    ;;;;;;;;.        .;;;;;;;;=          *;;;;;;;;.       ;;;;;;;;.   .;;;;;;;;      ",
+    "@@@@@@@@@@;;;;;;;;;;;;;$@@@@@@@@#;;;;;;;;;;;;;;;;;;;*@@@@@@@@@=;;;;;;;;;;;;=+@@@@@@@@.           +;;;;;;;;-      -;;;;;;;;;;;;;&    +;;;;;;;;*        .;;;;;;;;     *;;;;;;;&        .;;;;;;;;=          *;;;;;;;;+      #;;;;;;;-     ;;;;;;;;+     ",
+    "@@@@@@@@@@;;;;;;;;;;;;;$@@@@@@@@#;;;;;;;;;;;;;;;;;;;*@@@@@@@@@#$$&*-;;;;;;;=+@@@@@@@@.           +;;;;;;;;-      -;;;;;;;;;;;;;&    +;;;;;;;;*        $;;;;;;;=     .;;;;;;;;        .;;;;;;;;=          *;;;;;;;;+      *;;;;;;;&     %;;;;;;;*     ",
+    "@@@@@@@@@@;;;;;;;;;;;;;$@@@@@@@@@;;;;;;;;;;;;;;;;;;;&@@@@@@@@@@@@@@+@=;;;;;=@@@@@@@@@.           .;;;;;;;;-      -;;;;;;;;;;;;;&    +;;;;;;;;*        =;;;;;;;%      -;;;;;;;#       .;;;;;;;;=          *;;;;;;;;.      ;;;;;;;;+      ;;;;;;;;.    ",
+    "+@@@@@@@@@;;;;;;;;;;;;;%@@@@@@@@+-;;;;;;;;;;;;;;;;;;$@@@@@@@@@@@@@@@@@#-;;;=@@@@@@@@@.           .;;;;;;;;-      -;;;;;;;;;;;;;&    +;;;;;;;;*       .;;;;;;;;.      &;;;;;;;*       .;;;;;;;;=          =;;;;;;;;.     %;;;;;;;-       =;;;;;;;#    ",
+    "+@@@@@@@@@;;;;;;;;;;;;;&@@@@@@@@@%;;;;;;;;;;;;;;;;;;+@@@@@+++++++@@@@@@@&;;*@@@@@@@@@.           .;;;;;;;;-      @%%%%*;;;;;;;;&    +;;;;;;;;*       &;;;;;;;=.......#;;;;;;;;       .;;;;;;;;=          =;;;;;;;;.     -;;;;;;;%.......&;;;;;;;=    ",
+    "+@@@@@@@@@;;;;;;;;;;;;;*+@@@@@@@@@;;;;;;;;;;;;;;;;;&@@@+#%*-;;-=&$@@@@@@@%;&@@@@@@@@@             ;;;;;;;;;           %;;;;;;;;%    +;;;;;;;;*       -;;;;;;;;;;;;;;;;;;;;;;;;%      .;;;;;;;;=          -;;;;;;;;      ;;;;;;;;;;;;;;;;;;;;;;;;;.   ",
+    ".@@@@@@@@@-;;;;;;;;;;;;;+@@@@@@@@@%;;;;;;;;;;;;;;;;+@@@*;;;;;;;;;;;%@@@@@@&%@@@@@@@@@             ;;;;;;;;;           &;;;;;;;;%    +;;;;;;;;*       ;;;;;;;;;;;;;;;;;;;;;;;;;=      .;;;;;;;;=          ;;;;;;;;;     %;;;;;;;;;;;;;;;;;;;;;;;;;&   ",
+    ".@@@@@@@@@=;;;;;;;;;;;;;$@@@@@@@@@+*;;;;;;;;;;;;;;#@+%;;;;;;;;;;;;;;-@@@@@@@@@@@@@@@@             -;;;;;;;;#          =;;;;;;;;@    +;;;;;;;;*      *;;;;;;;;;;;;;;;;;;;;;;;;;;      .;;;;;;;;=         $;;;;;;;;-     ;;;;;;;;;;;;;;;;;;;;;;;;;;-   ",
+    ".@@@@@@@@@%;;;;;;;;;;;;;=@@@@@@@@@@+*;;;;;;;;;;;;#@+*;;;;;;;;;;;;;;;;;$+@@@@@@@@@@@@@             =;;;;;;;;=         @;;;;;;;;;     +;;;;;;;;*      ;;;;;;;;;;;;;;;;;;;;;;;;;;;&     .;;;;;;;;=         -;;;;;;;;&    .;;;;;;;;;;;;;;;;;;;;;;;;;;;.  ",
+    " @@@@@@@@@+;;;;;;;;;;;;;;@@@@@@@@@@@+%-;;;;;;;;*@@+*;;;;;;;;;;;;;;;;;;;$@@@@@@@@@@@@+             &;;;;;;;;;%       +;;;;;;;;;=     +;;;;;;;;*     +;;;;;;;;;;;;;;;;;;;;;;;;;;;;     .;;;;;;;;=       +-;;;;;;;;;$    &;;;;;;;;;;;;;;;;;;;;;;;;;;;*  ",
+    " @@@@@@@@@+-;;;;;;;;;;;;;*@@@@@@@@@@@@+$=;;;-&@@@@&;;;;;;;;;;;;;;;;;;;;;@@@@@@@@@@@@.             .;;;;;;;;;;-@   .*;;;;;;;;;;%     +;;;;;;;;*     *;;;;;;;;;;;;;;;;;;;;;;;;;;;;.    .;;;;;;;;-&&&&&*;;;;;;;;;;;;     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;  ",
+    " +@@@@@@@@@*;;;;;;;;;;;;;;#@@@@@@@@@@@@@@@@@+@@@@@;;;;;;;;;;;;;;;;;;;;;;-@@@@@@@@@@@               &;;;;;;;;;;;;;;;;;;;;;;;;;;      +;;;;;;;;*    .;;;;;;;;;;;;;;;;;;;;;;;;;;;;;&    .;;;;;;;;;;;;;;;;;;;;;;;;;;$    #;;;;;;;;;;;;;;;;;;;;;;;;;;;;;. ",
+    " .@@@@@@@@@#;;;;;;;;;;;;;;-@@@@@@@@@@@@@@@@@@@@@@-;;;;;;;;;;;;;;;;;;;;;;;$@@@@@@@@@@               .;;;;;;;;;;;;;;;;;;;;;;;;;@      +;;;;;;;;*    $;;;;;;;;%            ;;;;;;;;;    .;;;;;;;;;;;;;;;;;;;;;;;;;-     *;;;;;;;;            .;;;;;;;;* ",
+    "  @@@@@@@@@+-;;;;;;;;;;;;;;*+@@@@@@@@@@@@@@@@@@@#;;;;;;;;;;;;;;;;;;;;;;;;-+@@@@@@@@+                @;;;;;;;;;;;;;;;;;;;;;;;&       +;;;;;;;;*    *;;;;;;;;             &;;;;;;;;#   .;;;;;;;;;;;;;;;;;;;;;;;;;      ;;;;;;;;=             -;;;;;;;;.",
+    "  +@@@@@@@@@%;;;;;;;;;;;;;;;&@@@@@@@@@@@@@@@@@@+%;;;;;;;;;;;;;;;;;;;;;;;;;#@@@@@@@@.                 #;;;;;;;;;;;;;;;;;;;;;%        +;;;;;;;;*    ;;;;;;;;*              ;;;;;;;;*   .;;;;;;;;;;;;;;;;;;;;;;;-      %;;;;;;;;%             %;;;;;;;;#",
+    "  .@@@@@@@@@@;;;;;;;;;;;;;;;;&+@@@@@@@@@@@@@@@@+-;;;;;;;;;;;;;;;;;;;;;;;;;%@@@@@@@@                   .-;;;;;;;;;;;;;;;;;;+         +;;;;;;;;*   &;;;;;;;;$              =;;;;;;;;   .;;;;;;;;;;;;;;;;;;;;;;%       -;;;;;;;;               ;;;;;;;;*",
+    "   @@@@@@@@@@%;;;;;;;;;;;;;;;;*@@@@@@@@@@@@@@@@+;;;;;;;;;;;;;;;;;;;;;;;;;;&+@@@@@@+                     $=;;;;;;;;;;;;;=$           .;;;;;;;;*   =;;;;;;;;               $;;;;;;;;#  .;;;;;;;;;;;;;;;;;;;=%.        ;;;;;;;;&               *;;;;;;;;",
+    "   +@@@@@@@@@@-;;;;;;;;;;;;;;;;-#+@@@@@@@@@@@@@#;;;;;;;;;;;;;;;;;;;;;;;;;;*+@@@@@@.                       .%=---;---=%.              %&&&&&&&.   +&&&&&&&.                $&&&&&&%    #&&&&&&&&&&&&&&%$+            $&&&&&&%                 %&&&&&&#",
+    "   .@@@@@@@@@@#;;;;;;;;;;;;;;;;;;*@@@@@@@@@@@@@#;;;;;;;;;;;;;;;;;;;;;;;;;;=+@@@@@@                                                                                                                                                                   ",
+    "    +@@@@@@@@@@*;;;;;;;;;;;;;;;;;;;=%++@@@@@@@+@;;;;;;;;;;;;;;;;;;;;;;;;;;*+@@@@@.                                                                                                                                                                   ",
+    "    .@@@@@@@@@@@-;;;;;;;;;;;;;;;;;;;;;=&%%$%%&*-;;;;;;;;;;;;;;;;;;;;;;;;;;&+@@@@@                                                                                                                                                                    ",
+    "     +@@@@@@@@@@#;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;%@@@@@.                                                                                                                                                                    ",
+    "      @@@@@@@@@@@$;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;#@@@@+                                                                                                                                                                     ",
+    "      +@@@@@@@@@@@%;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-+@@@@                                                                                                                                                                      ",
+    "       @@@@@@@@@@@+&;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;$@@@@.                                                                                                                                                                      ",
+    "       .@@@@@@@@@@@+%;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-@@@@@                                                                                                                                                                       ",
+    "        .@@@@@@@@@@@@$;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;@@@@@                                                                                                                                                                        ",
+    "         +@@@@@@@@@@@@#-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;$@@@@.                                                                                                                                                                        ",
+    "          @@@@@@@@@@@@@@*;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;%+@@@+                                                                                                                                                                         ",
+    "           @@@@@@@@@@@@@+#-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-#@@@@+                                                                                                                                                                          ",
+    "           .@@@@@@@@@@@@@@@%;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;%@@@@@@                                                                                                                                                                           ",
+    "            .@@@@@@@@@@@@@@@@$-;;;;;;;;;;;;;;;;;;;;;;;;*$%*-;;;-*$@@@@@@@                                                                                                                                                                            ",
+    "             .@@@@@@@@@@@@@@@@+#&-;;;;;;;;;;;;;;;;;;=%@+@@+++.+++@@@@@@+                                                                                                                                                                             ",
+    "               @@@@@@@@@@@@@@@@@@++%*--;;;;;;;;--&#++@@@@@@@@@@@@@@@@@+                                                                                                                                                                              ",
+    "                @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                               ",
+    "                 .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                ",
+    "                  .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+                                                                                                                                                                                  ",
+    "                    +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                   ",
+    "                      @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                     ",
+    "                       .+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                       ",
+    "                         .+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                         ",
+    "                            .+@@@@@@@@@@@@@@@@@@@@@@@@@@+.                                                                                                                                                                                           ",
+    "                               .+@@@@@@@@@@@@@@@@@@@@.                                                                                                                                                                                               ",
+    "                                   ...+++@@@@++...                                                                                                                                                                                                   "};
+
+const char* loopRepeat_xpm[] = {
+    "18 18 8 1",
+    "  c #181917",
+    ". c #242523",
+    "+ c #323331",
+    "@ c #4D4F4C",
+    "# c #646663",
+    "$ c #787A77",
+    "% c #919390",
+    "& c #BFC1BD",
+    "..................",
+    "..................",
+    "..................",
+    "...&%#......#%&...",
+    "...&&&%+..+%&&&...",
+    "...$%&&%..%&&%$...",
+    ".....$&&##&&$.....",
+    "......%&%%&%......",
+    "......$&&&&$......",
+    "......$&&&&$......",
+    "......%&%%&%......",
+    ".....$&&##&&$.....",
+    "...$%&&%..%&&%$...",
+    "...&&&%+..+%&&&...",
+    "...&%#......#%&...",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* loopBasic_xpm[] = {
+    "18 18 8 1",
+    "  c #181917",
+    ". c #242523",
+    "+ c #313230",
+    "@ c #4D4F4C",
+    "# c #666765",
+    "$ c #787A77",
+    "% c #919390",
+    "& c #BEC0BD",
+    "..................",
+    "..................",
+    "..................",
+    "......#%&&%#......",
+    "....+%&&&&&&%+....",
+    "....%&&&%%&&&%....",
+    "...#&&%+..+%&&#...",
+    "...%&&+....+&&%...",
+    "...&&%......%&&...",
+    "...&&%......%&&...",
+    "...%&&+....+&&%...",
+    "...#&&%+..+%&&#...",
+    "....%&&&%%&&&%....",
+    "....+%&&&&&&%+....",
+    "......#%&&%#......",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* loopOnce_xpm[] = {
+    "18 18 8 1",
+    "  c #181917",
+    ". c #242523",
+    "+ c #323331",
+    "@ c #4D4F4C",
+    "# c #646663",
+    "$ c #787A77",
+    "% c #919390",
+    "& c #BFC1BD",
+    "..................",
+    "..................",
+    "..................",
+    "......$%&&%#......",
+    "....+&&&&&&&&+....",
+    "...+&&&&$$&&&&+...",
+    "...$&&$....$&&$...",
+    "...%&&......%&%...",
+    "..................",
+    "..................",
+    "...%&&+.....&&&...",
+    "...#&&$....$&&#...",
+    "....%&&&%$&&&%....",
+    "....+%&&&&&&%+....",
+    "......#%&&%#......",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* loopOnceBar_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #393A38",
+    "+ c #545553",
+    "@ c #747673",
+    "# c #A3A5A2",
+    "$ c #ADAFAC",
+    "% c #B5B7B4",
+    "& c #C7C9C6",
+    "                  ",
+    "                  ",
+    "                  ",
+    "      @$&%#@      ",
+    "    .$&&&&&&$.    ",
+    "    %&&#@@#&&$    ",
+    "   @&&@    @&&@   ",
+    "   %&# +%$+ #&$   ",
+    "       %&&%       ",
+    "       %&&%       ",
+    "   $&# +%%+ #&$   ",
+    "   @&&@    @&&@   ",
+    "    $&&#@@#&&$    ",
+    "    .$&&&&&&$.    ",
+    "      @#&&#@      ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* oneshotBasic_xpm[] = {
+    "18 18 8 1",
+    "  c #181917",
+    ". c #242523",
+    "+ c #313230",
+    "@ c #4D4F4C",
+    "# c #666765",
+    "$ c #787A77",
+    "% c #919390",
+    "& c #BEC0BD",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "...$$$$$$$$$$$$...",
+    "...&&&&&&&&&&&&...",
+    "...&&&&&&&&&&&&...",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* oneshotBasicPause_xpm[] = {
+    "18 18 5 1",
+    "  c #252525",
+    ". c #4E4E4E",
+    "+ c #767676",
+    "@ c #9F9F9F",
+    "# c #C8C8C8",
+    "                  ",
+    "                  ",
+    "                  ",
+    "            +##   ",
+    "            +##   ",
+    "            +##   ",
+    "            +##   ",
+    "            +##   ",
+    "            +##   ",
+    "            +##   ",
+    "            +##   ",
+    "            +##   ",
+    "   +++++++++@##   ",
+    "   ############   ",
+    "   ############   ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* oneshotRetrig_xpm[] = {
+    "18 18 8 1",
+    "  c #181917",
+    ". c #242523",
+    "+ c #313230",
+    "@ c #4D4F4C",
+    "# c #666765",
+    "$ c #787A77",
+    "% c #919390",
+    "& c #BEC0BD",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "...$$$$$$$#@......",
+    "...&&&&&&&&&&@....",
+    "...&&&&&&&&&&&+...",
+    "..........+$&&%...",
+    "............%&&...",
+    "............%&&...",
+    "...........+&&%...",
+    "...$$$$$$$%&&&#...",
+    "...&&&&&&&&&&%....",
+    "...&&&&&&&&%#.....",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* oneshotPress_xpm[] = {
+    "18 18 8 1",
+    "  c #181917",
+    ". c #242523",
+    "+ c #313230",
+    "@ c #4D4F4C",
+    "# c #666765",
+    "$ c #787A77",
+    "% c #919390",
+    "& c #BEC0BD",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "...+%&%+..........",
+    "...%&&&%..........",
+    "...&&&&&..........",
+    "...$&&&$..........",
+    "...+$&$+..........",
+    "..................",
+    "...$$$$$$$$$$$$...",
+    "...&&&&&&&&&&&&...",
+    "...&&&&&&&&&&&&...",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* oneshotEndless_xpm[] = {
+    "18 18 6 1",
+    "  c #242523",
+    ". c #464745",
+    "+ c #6D6F6C",
+    "@ c #888A87",
+    "# c #ADAFAC",
+    "$ c #C6C8C5",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "        .++.      ",
+    "       @$$$$#.    ",
+    "      @$$$$$$$.   ",
+    "     .$$#. +$$@   ",
+    "     +$$.   @$#   ",
+    "     +$$    @$$   ",
+    "     .$$+   #$#   ",
+    "   @@@$$$@@#$$+   ",
+    "   $$$$$$$$$$@    ",
+    "   $$$$$$$$#+     ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* updirOff_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #332F2E",
+    "+ c #54494A",
+    "@ c #6B5A5C",
+    "# c #866C6B",
+    "$ c #967B7A",
+    "% c #987D7C",
+    "& c #B18E8F",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "        @@        ",
+    "       #&&#       ",
+    "     .#&&&&#.     ",
+    "    .$&&&&&&$.    ",
+    "    +@%&&&&%@+    ",
+    "      #&&&&#      ",
+    "      #&&&&#      ",
+    "      #&&&&#      ",
+    "      #&&&&#      ",
+    "      #&&&&#      ",
+    "       ....       ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* updirOn_xpm[] = {
+    "18 18 8 1",
+    "  c #4D4F4C",
+    ". c #555150",
+    "+ c #706465",
+    "@ c #7D6B6E",
+    "# c #877373",
+    "$ c #957978",
+    "% c #9F8382",
+    "& c #B18E8F",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "        ##        ",
+    "       #&&#       ",
+    "     .$&&&&$.     ",
+    "    .%&&&&&&%.    ",
+    "    +@%&&&&%@+    ",
+    "      $&&&&$      ",
+    "      $&&&&$      ",
+    "      $&&&&$      ",
+    "      $&&&&$      ",
+    "      $&&&&$      ",
+    "       ....       ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* pause_xpm[] = {
+    "23 23 8 1",
+    "  c #4D4F4C",
+    ". c #514E53",
+    "+ c #5C4F61",
+    "@ c #6F507E",
+    "# c #855098",
+    "$ c #9551AE",
+    "% c #A652C5",
+    "& c #AE52D1",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "       #+              ",
+    "       &%#.            ",
+    "       &&&%@           ",
+    "       &&&&&$+         ",
+    "       &&&&&&&#.       ",
+    "       &&&&&&&&%@      ",
+    "       &&&&&&&&&&#     ",
+    "       &&&&&&&&%@.     ",
+    "       &&&&&&&#.       ",
+    "       &&&&&$+         ",
+    "       &&&%@           ",
+    "       &&#.            ",
+    "       $+              ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       "};
+
+const char* play_xpm[] = {
+    "23 23 8 1",
+    "  c #242523",
+    ". c #393534",
+    "+ c #574B4C",
+    "@ c #6E5B5A",
+    "# c #7C6663",
+    "$ c #8C7170",
+    "% c #A48384",
+    "& c #B18E8F",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "       $.              ",
+    "       &&@             ",
+    "       &&&%+           ",
+    "       &&&&&$.         ",
+    "       &&&&&&&@        ",
+    "       &&&&&&&&%+      ",
+    "       &&&&&&&&&&#     ",
+    "       &&&&&&&&%+      ",
+    "       &&&&&&&#.       ",
+    "       &&&&&$.         ",
+    "       &&&%+           ",
+    "       &&@             ",
+    "       $.              ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       "};
+
+const char* rewindOff_xpm[] = {
+    "23 23 8 1",
+    "  c #242523",
+    ". c #393534",
+    "+ c #574B4C",
+    "@ c #6E5B5A",
+    "# c #7C6663",
+    "$ c #8C7170",
+    "% c #A48384",
+    "& c #B18E8F",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                .$     ",
+    "               @&&     ",
+    "             +%&&&     ",
+    "           .$&&&&&     ",
+    "          @&&&&&&&     ",
+    "        +%&&&&&&&&     ",
+    "       #&&&&&&&&&&     ",
+    "        +%&&&&&&&&     ",
+    "         .#&&&&&&&     ",
+    "           .$&&&&&     ",
+    "             +%&&&     ",
+    "               @&&     ",
+    "                .$     ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       "};
+
+const char* rewindOn_xpm[] = {
+    "23 23 8 1",
+    "  c #4D4F4C",
+    ". c #514E53",
+    "+ c #5C4F61",
+    "@ c #6F507E",
+    "# c #855098",
+    "$ c #9551AE",
+    "% c #A652C5",
+    "& c #AE52D1",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                +#     ",
+    "              .#%&     ",
+    "             @%&&&     ",
+    "           +$&&&&&     ",
+    "         .#&&&&&&&     ",
+    "        @%&&&&&&&&     ",
+    "       #&&&&&&&&&&     ",
+    "       .@%&&&&&&&&     ",
+    "         .#&&&&&&&     ",
+    "           +$&&&&&     ",
+    "             @%&&&     ",
+    "              .#&&     ",
+    "                +$     ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       "};
+
+const char* giada_icon[] = {
+    "65 65 8 1",
+    "  c #444643",
+    ". c #565755",
+    "+ c #6C6E6B",
+    "@ c #898B88",
+    "# c #A3A5A2",
+    "$ c #BEC0BD",
+    "% c #D7DAD6",
+    "& c #FCFEFB",
+    "                                                                 ",
+    " &&&&&&&&&&&&&&&&&&&&%$@                .@$%&&&&&&&&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&&&&&%$@.                     .#%&&&&&&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&&&&#+                          .@$&&&&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&&$+                              .@%&&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&@                                  .#&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&%.                                     @&&&&&&&&&&&& ",
+    " &&&&&&&&&&$              ...+@@#@@+..              +%&&&&&&&&&& ",
+    " &&&&&&&&&#            .+#$&&&&&&&&&&%$@+.           .%&&&&&&&&& ",
+    " &&&&&&&&#           +#%&&&&&&&&&&&&&&&&&$@.          .%&&&&&&&& ",
+    " &&&&&&&#          .#%&&&&&&&&&&&&&&&&&&&&&%@          .%&&&&&&& ",
+    " &&&&&&$          @%&&&&&&&&&&&&&&&&&&&&&&&&&%+         .&&&&&&& ",
+    " &&&&&%          $&&&&&&&&&&&&&&&&&&&&&&&&&&&&&@         +&&&&&& ",
+    " &&&&&.        .%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#         @&&&&& ",
+    " &&&&@        +%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#         $&&&& ",
+    " &&&$        .%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#        +&&&& ",
+    " &&&+        %&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#        #&&& ",
+    " &&#        $&&&&&&&&&&&&&&&&%$###$%&&&&&&&&&&&&&&&&@       +%&& ",
+    " &%+       @&&&&&&&&&&&&&&%@..    ..+#&&&&&&&&&&&&&&%.       #&& ",
+    " &$       .%&&&&&&&&&&&&%+             #&&&&&&&&&&&&&#       +&& ",
+    " &@       $&&&&&&&&&&&&#                +%&&&&&&&&&&&%+       $& ",
+    " %.      +%&&&&&&&&&&&@                  .$&&&&&&&&&&&#       @& ",
+    " $       #&&&&&&&&&&&@                    .$&&&&&&&&&&&+       & ",
+    " @      .%&&&&&&&&&&#                      .%&&&&&&&&&&@       $ ",
+    " .      +&&&&&&&&&&%                        +&&&&&&&&&&%.      @ ",
+    "        #&&&&&&&&&&+         @%&&%$+         #&&&&&&&&&&.      . ",
+    "       .%&&&&&&&&&$        +%&&&&&&&#        .&&&&&&&&&&@        ",
+    "       .&&&&&&&&&&+       +&&&&&&&&&&$        $&&&&&&&&&$        ",
+    "       +&&&&&&&&&&       .%&&&&&&&&&&&#       @&&&&&&&&&%        ",
+    "       @&&&&&&&&&$       @&&&&&&&&&&&&&.      +&&&&&&&&&&        ",
+    "       @&&&&&&&&&#       %&&&&&&&&&&&&&#      .%&&&&&&&&&.       ",
+    "       #&&&&&&&&&@      .&&&&&&&&&&&&&&$      .%&&&&&&&&&.       ",
+    "       #&&&&&&&&&@      .&&&&&&&&&&&&&&%      .#$%&&&&&&&+       ",
+    "       #&&&&&&&&&@      .&&&&&&&&&&&&&&$           @%&&&&.       ",
+    "       @&&&&&&&&&#       %&&&&&&&&&&&&&#             @%&&.       ",
+    "       @&&&&&&&&&$       #&&&&&&&&&&&&&.   +@@@@+.    .$&        ",
+    "       +&&&&&&&&&&       .%&&&&&&&&&&&#  +$%&&&&&%#.   .$        ",
+    "       .&&&&&&&&&&+       +&&&&&&&&&&$  $&&&&&&&&&&%@   .        ",
+    "       .%&&&&&&&&&$        +%&&&&&&&#. %&&&&&&&&&&&&&#           ",
+    "        #&&&&&&&&&&+         @%&&&$+  $&&&&&&&&&&&&&&&@        . ",
+    " .      +&&&&&&&&&&%                 @&&&&&&&&&&&&&&&&%.       @ ",
+    " @      .%&&&&&&&&&&#                %&&&&&&&&&&&&&&&&&@       $ ",
+    " $       #&&&&&&&&&&&@              @&&&&&&&&&&&&&&&&&&%.      & ",
+    " %.      +%&&&&&&&&&&&@             $&&&&&&&&&&&&&&&&&&&.     @& ",
+    " &@       $&&&&&&&&&&&&#            %&&&&&&&&&&&&&&&&&&&+     $& ",
+    " &$       .%&&&&&&&&&&&&%+          %&&&&&&&&&&&&&&&&&&&@    +&& ",
+    " &%+       @&&&&&&&&&&&&&&%@..     .%&&&&&&&&&&&&&&&&&&&@    #&& ",
+    " &&#        $&&&&&&&&&&&&&&&&%$###$%&&&&&&&&&&&&&&&&&&&&+   +%&& ",
+    " &&&+       .%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&.   #&&& ",
+    " &&&$        .%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#   +&&&& ",
+    " &&&&@        +%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&+   $&&&& ",
+    " &&&&&.        .%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#   @&&&&& ",
+    " &&&&&%         .$&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%.  +&&&&&& ",
+    " &&&&&&$          @%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%.  .&&&&&&& ",
+    " &&&&&&&#          .$%&&&&&&&&&&&&&&&&&&&&&&&&&&&&%#   .%&&&&&&& ",
+    " &&&&&&&&#           +#%&&&&&&&&&&&&&&&&&$@#$%%$$@+   .%&&&&&&&& ",
+    " &&&&&&&&&#            .+#%&&&&&&&&&&&$@+.  ....     .%&&&&&&&&& ",
+    " &&&&&&&&&&$              ..+@@###@+...             +%&&&&&&&&&& ",
+    " &&&&&&&&&&&%.                                     @&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&@                                  .#&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&&$+                              .@%&&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&&&&#+                          .@$&&&&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&&&&&%$@.                     .#%&&&&&&&&&&&&&&&&&& ",
+    " &&&&&&&&&&&&&&&&&&&&%$@                .@$%&&&&&&&&&&&&&&&&&&&& ",
+    "                                                                 "};
+
+const char* recOff_xpm[] = {
+    "23 23 8 1",
+    "  c #242523",
+    ". c #342F2E",
+    "+ c #3F3B3A",
+    "@ c #594F4F",
+    "# c #7A6663",
+    "$ c #8C7170",
+    "% c #A68384",
+    "& c #B18E8F",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "         @$%%$@        ",
+    "       .$&&&&&&$.      ",
+    "       $&&&&&&&&$      ",
+    "      @&&&#++#&&&@     ",
+    "      $&&#    #&&$     ",
+    "      %&&+    +&&%     ",
+    "      %&&+    +&&%     ",
+    "      $&&#    #&&$     ",
+    "      @&&&#++#&&&@     ",
+    "       $&&&&&&&&$      ",
+    "       .$&&&&&&$.      ",
+    "         @$%%$@        ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       "};
+
+const char* recOn_xpm[] = {
+    "23 23 8 1",
+    "  c #4D4F4C",
+    ". c #5F4E50",
+    "+ c #6E4F50",
+    "@ c #8C5050",
+    "# c #AE5454",
+    "$ c #BB5253",
+    "% c #C55352",
+    "& c #E85557",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "         @$&&$@        ",
+    "       .%&&&&&&%.      ",
+    "       %&&&&&&&&%      ",
+    "      @&&&#++#&&&@     ",
+    "      $&&#    #&&$     ",
+    "      &&&+    +&&&     ",
+    "      &&&+    +&&&     ",
+    "      $&&#    #&&$     ",
+    "      @&&&#++#&&&@     ",
+    "       %&&&&&&&&%      ",
+    "       .%&&&&&&%.      ",
+    "         @$&&$@        ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       "};
+
+const char* inputRecOn_xpm[] = {
+    "23 23 8 1",
+    "  c #524D4C",
+    ". c #4D4F4C",
+    "+ c #5D4F50",
+    "@ c #8C5050",
+    "# c #BB5253",
+    "$ c #C45251",
+    "% c #DD5256",
+    "& c #EA5657",
+    ".......................",
+    ".......................",
+    ".......................",
+    ".......................",
+    ".......................",
+    "........ @#%%#@ .......",
+    ".......+$&&&&&&$+......",
+    "...... $&&&&&&&&$ .....",
+    "......@&&&&&&&&&&@.....",
+    "......#&&&&&&&&&&#.....",
+    "......%&&&&&&&&&&%.....",
+    "......%&&&&&&&&&&%.....",
+    "......#&&&&&&&&&&#.....",
+    "......@&&&&&&&&&&@.....",
+    ".......$&&&&&&&&$......",
+    ".......+$&&&&&&$+......",
+    "........ @#%%#@ .......",
+    ".......................",
+    ".......................",
+    ".......................",
+    ".......................",
+    ".......................",
+    "......................."};
+
+const char* inputRecOff_xpm[] = {
+    "23 23 8 1",
+    "  c #242523",
+    ". c #252724",
+    "+ c #332F2E",
+    "@ c #594E4F",
+    "# c #896E6D",
+    "$ c #8D7271",
+    "% c #A68384",
+    "& c #B18E8F",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "        .@#%%#@.       ",
+    "       +$&&&&&&$+      ",
+    "      .$&&&&&&&&$.     ",
+    "      @&&&&&&&&&&@     ",
+    "      #&&&&&&&&&&#     ",
+    "      %&&&&&&&&&&%     ",
+    "      %&&&&&&&&&&%     ",
+    "      #&&&&&&&&&&#     ",
+    "      @&&&&&&&&&&@     ",
+    "       $&&&&&&&&$      ",
+    "       +$&&&&&&$+      ",
+    "        .@#%%#@.       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       ",
+    "                       "};
+
+const char* freeInputRecOff_xpm[] = {
+    "13 23 9 1",
+    "  c None",
+    ". c #232523",
+    "+ c #353130",
+    "@ c #483E3F",
+    "# c #4D4F4C",
+    "$ c #574D4D",
+    "% c #705D5C",
+    "& c #8C7271",
+    "* c #AF8D8E",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".....+%%.....",
+    ".....@*&.....",
+    ".....@*&.....",
+    ".....@*&.....",
+    "....$@$@.....",
+    "...+**&@.....",
+    "....+%**&@...",
+    "......+%*$...",
+    ".....@&%+....",
+    ".....@*&.....",
+    ".....@*&.....",
+    ".....@*&.....",
+    ".....+@@.....",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    "............."};
+
+const char* freeInputRecOn_xpm[] = {
+    "13 23 9 1",
+    "  c None",
+    ". c #4D4F4C",
+    "+ c #575352",
+    "@ c #5D5857",
+    "# c #6A5F5F",
+    "$ c #796A6B",
+    "% c #877472",
+    "& c #977C7C",
+    "* c #B08E8F",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".....@%$.....",
+    ".....#*&.....",
+    ".....#*&.....",
+    ".....#*&.....",
+    "....##$#.....",
+    "...+**&@.....",
+    "....@%**&@...",
+    "......@%*$...",
+    ".....@&%@....",
+    ".....#*&.....",
+    ".....#*&.....",
+    ".....#*&.....",
+    ".....+##.....",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    "............."};
+
+const char* muteOff_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #2E2F2D",
+    "+ c #3B3C3A",
+    "@ c #525451",
+    "# c #6F716E",
+    "$ c #878986",
+    "% c #ADAFAC",
+    "& c #C6C8C5",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "     ++.  .++     ",
+    "    +&&$  $&&+    ",
+    "    +&&%  %&&+    ",
+    "    +&%&++&%&+    ",
+    "    +&$&##&$&+    ",
+    "    +&#%$$%#&+    ",
+    "    +&#$%%$#&+    ",
+    "    +&#@&&@#&+    ",
+    "    +&#+&&+#&+    ",
+    "    .#@ ## @#.    ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* muteOn_xpm[] = {
+    "18 18 8 1",
+    "  c #4D4F4C",
+    ". c #585A57",
+    "+ c #616260",
+    "@ c #7A7C79",
+    "# c #888A87",
+    "$ c #989A97",
+    "% c #B2B4B1",
+    "& c #C6C8C5",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "     ..    ..     ",
+    "    +&&$  $&&+    ",
+    "    +&&%  %&&+    ",
+    "    +&%&++&%&+    ",
+    "    +&$&@@&$&+    ",
+    "    +&#%$$%#&+    ",
+    "    +&#$&&$#&+    ",
+    "    +&#@&&@#&+    ",
+    "    +&#.&&.#&+    ",
+    "    .#+ ## +#.    ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* readActionOff_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #393B38",
+    "+ c #555754",
+    "@ c #6B6D6A",
+    "# c #7F807E",
+    "$ c #9C9E9B",
+    "% c #B1B3B0",
+    "& c #C3C5C2",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "     ....         ",
+    "     %&&&&%+      ",
+    "     %&@@@&&      ",
+    "     %%   $&.     ",
+    "     %&@@#&$      ",
+    "     %&&&&@       ",
+    "     %% +&$       ",
+    "     %%  #&#      ",
+    "     %%   %&+     ",
+    "     @@   .#+     ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* readActionOn_xpm[] = {
+    "18 18 8 1",
+    "  c #4D4F4C",
+    ". c #696B68",
+    "+ c #7A7C79",
+    "@ c #888A87",
+    "# c #939592",
+    "$ c #A7A9A6",
+    "% c #B7B9B6",
+    "& c #C4C6C3",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "     %&&&&%.      ",
+    "     %&++@&&      ",
+    "     %%   $&      ",
+    "     %&@@#&$      ",
+    "     %&&&&@       ",
+    "     %% +&$       ",
+    "     %%  #&#      ",
+    "     %%   %&.     ",
+    "     +@   .@+     ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* readActionDisabled_xpm[] = {
+    "18 18 7 1",
+    "  c None",
+    ". c #252525",
+    "+ c #313131",
+    "@ c #393939",
+    "# c #424242",
+    "$ c #4A4A4A",
+    "% c #585858",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    ".....@@@@+........",
+    ".....%%%%%%+......",
+    ".....%%#+$%$......",
+    ".....%%@.#%$......",
+    ".....%%##%%@......",
+    ".....%%%%%$.......",
+    ".....%%@+%%#......",
+    ".....%%@.@%%......",
+    ".....%%@..%%#.....",
+    ".....@@...+@@.....",
+    "..................",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* metronomeOff_xpm[] = {
+    "13 23 3 1",
+    "  c None",
+    ". c #252525",
+    "+ c #B18E8E",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    "............."};
+
+const char* metronomeOn_xpm[] = {
+    "13 23 3 1",
+    "  c None",
+    ". c #4E4E4E",
+    "+ c #B18E8E",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    "....+...+....",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    ".............",
+    "............."};
+
+const char* recTriggerModeOff_xpm[] = {
+    "13 23 8 1",
+    "  c #232523",
+    ". c #2A2625",
+    "+ c #43393A",
+    "@ c #514647",
+    "# c #6F5C59",
+    "$ c #8B7170",
+    "% c #AA8889",
+    "& c #B08E8F",
+    "             ",
+    "             ",
+    "             ",
+    "             ",
+    "             ",
+    "     @$@     ",
+    "     %&%     ",
+    "     $&$     ",
+    "     .+.     ",
+    "             ",
+    "     #%#     ",
+    "     %&%     ",
+    "     #%#     ",
+    "             ",
+    "     .+.     ",
+    "     $&$     ",
+    "     %&%     ",
+    "     @$@     ",
+    "             ",
+    "             ",
+    "             ",
+    "             ",
+    "             "};
+
+const char* recTriggerModeOn_xpm[] = {
+    "13 23 8 1",
+    "  c #4D4F4C",
+    ". c #534E4D",
+    "+ c #605B5A",
+    "@ c #6D6363",
+    "# c #817072",
+    "$ c #967C7B",
+    "% c #AC8A8B",
+    "& c #B08E8F",
+    "             ",
+    "             ",
+    "             ",
+    "             ",
+    "             ",
+    "     @$@     ",
+    "     %&%     ",
+    "     $&$     ",
+    "     .+.     ",
+    "             ",
+    "     #%#     ",
+    "     %&%     ",
+    "     #%#     ",
+    "             ",
+    "     .+.     ",
+    "     $&$     ",
+    "     %&%     ",
+    "     @$@     ",
+    "             ",
+    "             ",
+    "             ",
+    "             ",
+    "             "};
+
+const char* zoomInOff_xpm[] = {
+    "18 18 8 1",
+    "  c None",
+    ". c #252525",
+    "+ c #262626",
+    "@ c #535353",
+    "# c #ACACAC",
+    "$ c #AEAEAE",
+    "% c #B1B1B1",
+    "& c #C4C4C4",
+    "++++++++++++++++++",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+.......@@.......+",
+    "+.......#$.......+",
+    "+.......#$.......+",
+    "+....@%%&&%%@....+",
+    "+....@%%&&%%@....+",
+    "+.......#$.......+",
+    "+.......#$.......+",
+    "+.......@@.......+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "++++++++++++++++++"};
+
+const char* zoomInOn_xpm[] = {
+    "18 18 8 1",
+    "  c None",
+    ". c #4E4E4E",
+    "+ c #707070",
+    "@ c #717171",
+    "# c #B3B3B3",
+    "$ c #B5B5B5",
+    "% c #B7B7B7",
+    "& c #C5C5C5",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "........++........",
+    "........#$........",
+    "........#$........",
+    ".....@%%&&%%@.....",
+    ".....@%%&&%%@.....",
+    "........#$........",
+    "........#$........",
+    "........++........",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* zoomOutOff_xpm[] = {
+    "18 18 5 1",
+    "  c None",
+    ". c #252525",
+    "+ c #262626",
+    "@ c #9C9C9C",
+    "# c #BBBBBB",
+    "++++++++++++++++++",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+......@##@......+",
+    "+......@##@......+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "+................+",
+    "++++++++++++++++++"};
+
+const char* zoomOutOn_xpm[] = {
+    "18 18 4 1",
+    "  c None",
+    ". c #4E4E4E",
+    "+ c #A7A7A7",
+    "@ c #BEBEBE",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    ".......+@@+.......",
+    ".......+@@+.......",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* scrollRightOff_xpm[] = {
+    "12 12 8 1",
+    "  c #181917",
+    ". c #242523",
+    "+ c #2E2F2D",
+    "@ c #4D4F4C",
+    "# c #5D5F5C",
+    "$ c #828481",
+    "% c #9B9D9A",
+    "& c #BCBEBB",
+    "............",
+    "............",
+    "...+........",
+    "...&$@......",
+    "...$&&%@....",
+    "....+#%&%...",
+    "....+#%&%...",
+    "...$&&%#....",
+    "...&$@......",
+    "...+........",
+    "............",
+    "............"};
+
+const char* scrollLeftOff_xpm[] = {
+    "12 12 8 1",
+    "  c #181917",
+    ". c #242523",
+    "+ c #2E2F2D",
+    "@ c #4D4F4C",
+    "# c #5D5F5C",
+    "$ c #828481",
+    "% c #9B9D9A",
+    "& c #BCBEBB",
+    "............",
+    "............",
+    "........+...",
+    "......@$&...",
+    "....@%&&$...",
+    "...%&%#+....",
+    "...%&%#+....",
+    "....#%&&$...",
+    "......@$&...",
+    "........+...",
+    "............",
+    "............"};
+
+const char* scrollLeftOn_xpm[] = {
+    "12 12 8 1",
+    "  c #4D4F4C",
+    ". c #6B6D6A",
+    "+ c #7B7D7A",
+    "@ c #969895",
+    "# c #A6A8A5",
+    "$ c #B4B6B3",
+    "% c #C0C2BF",
+    "& c #FEFFFC",
+    "            ",
+    "            ",
+    "            ",
+    "      .@$   ",
+    "    +#%%@   ",
+    "   $%#+     ",
+    "   %%#+     ",
+    "    +$%%@   ",
+    "      .#$   ",
+    "            ",
+    "            ",
+    "            "};
+
+const char* scrollRightOn_xpm[] = {
+    "12 12 8 1",
+    "  c #4D4F4C",
+    ". c #6B6D6A",
+    "+ c #7B7D7A",
+    "@ c #969895",
+    "# c #A6A8A5",
+    "$ c #B4B6B3",
+    "% c #C0C2BF",
+    "& c #FEFFFC",
+    "            ",
+    "            ",
+    "            ",
+    "   %@.      ",
+    "   @%%#.    ",
+    "     +#%#   ",
+    "     +#%#   ",
+    "   @%%#+    ",
+    "   %@.      ",
+    "            ",
+    "            ",
+    "            "};
+
+const char* soloOn_xpm[] = {
+    "18 18 8 1",
+    "  c #4D4F4C",
+    ". c #616360",
+    "+ c #737572",
+    "@ c #838582",
+    "# c #929491",
+    "$ c #A5A7A4",
+    "% c #B1B3B0",
+    "& c #C6C8C5",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "       .@+.       ",
+    "      #&&&&#      ",
+    "     .&$  %&.     ",
+    "      &%+ ..      ",
+    "      #&&&$.      ",
+    "       .@$&&.     ",
+    "     .#.  @&@     ",
+    "     .&$. #&+     ",
+    "      #&&&&$      ",
+    "       .+@+       ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* soloOff_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #3D3F3D",
+    "+ c #525451",
+    "@ c #666865",
+    "# c #80827F",
+    "$ c #979996",
+    "% c #A7A9A6",
+    "& c #C6C8C5",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "       .@@.       ",
+    "      #&&&&#      ",
+    "     .&$  %&.     ",
+    "      &%+ ..      ",
+    "      #&&&$+      ",
+    "       .@%&&.     ",
+    "     +#.  @&@     ",
+    "     .&$..#&+     ",
+    "      #&&&&$      ",
+    "       .@@+       ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+#ifdef WITH_VST
+
+const char* fxOff_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #40423F",
+    "+ c #4D4E4C",
+    "@ c #686A67",
+    "# c #7B7D7A",
+    "$ c #919390",
+    "% c #AEB0AD",
+    "& c #C1C3C0",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "   ..... .   .    ",
+    "   $&&&$ $% @&.   ",
+    "   $$    .&#&@    ",
+    "   $%##.  @&$     ",
+    "   $%##.  #&%     ",
+    "   $$    .&@&#    ",
+    "   $$    %$ @&.   ",
+    "   ..    +   +.   ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* fxOn_xpm[] = {
+    "18 18 8 1",
+    "  c #4D4F4C",
+    ". c #565855",
+    "+ c #636562",
+    "@ c #80827F",
+    "# c #8E908D",
+    "$ c #9FA19E",
+    "% c #B1B3B0",
+    "& c #C1C3C0",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "   .++++ +.  +.   ",
+    "   $&&&$ $% @&.   ",
+    "   $$    .&#&@    ",
+    "   $%##+  @&$     ",
+    "   $%##+  #&%     ",
+    "   $$    +&@&#    ",
+    "   $$    %$ @&+   ",
+    "   ++   .+.  ++   ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* fxShiftUpOff_xpm[] = {
+    "18 18 7 1",
+    "  c #242523",
+    ". c #4D4F4C",
+    "+ c #A3A5A2",
+    "@ c #868885",
+    "# c #C1C3C0",
+    "$ c #313330",
+    "% c #626361",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "       .+@        ",
+    "       @+#.       ",
+    "      $#%+@       ",
+    "      %# %#$      ",
+    "      +@ $#%      ",
+    "     $#.  @+      ",
+    "     $.   $.      ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* fxShiftUpOn_xpm[] = {
+    "18 18 5 1",
+    "  c #4D4F4C",
+    ". c #70726F",
+    "+ c #A5A7A4",
+    "@ c #C1C3BF",
+    "# c #8E908D",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "       .++        ",
+    "       +@@.       ",
+    "       @.+#       ",
+    "      .@ .@       ",
+    "      +#  @.      ",
+    "     .@.  #+      ",
+    "      .    .      ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* fxShiftDownOff_xpm[] = {
+    "18 18 7 1",
+    "  c #242523",
+    ". c #4D4F4C",
+    "+ c #A3A5A2",
+    "@ c #313330",
+    "# c #626361",
+    "$ c #868885",
+    "% c #C1C3C0",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "     .+@  #$      ",
+    "     @%#  +$      ",
+    "      $+ .%@      ",
+    "      .%@$+       ",
+    "       +$%#       ",
+    "       #%%@       ",
+    "       @..        ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* fxShiftDownOn_xpm[] = {
+    "18 18 5 1",
+    "  c #4D4F4C",
+    ". c #70726F",
+    "+ c #A5A7A4",
+    "@ c #C1C3BF",
+    "# c #8E908D",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "     .+   .+      ",
+    "      @.  +#      ",
+    "      #+ .@.      ",
+    "      .@.#+       ",
+    "       +#@.       ",
+    "       #@@        ",
+    "        ..        ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* vstLogo_xpm[] = {
+    "65 38 8 1",
+    "  c #161715",
+    ". c #2B2D2A",
+    "+ c #474846",
+    "@ c #6A6C69",
+    "# c #8C8E8B",
+    "$ c #A8AAA7",
+    "% c #C7C9C6",
+    "& c #EEF0ED",
+    " @#############################################################+ ",
+    "@#.............................................................$+",
+    "#.                                                             .#",
+    "#.                                                             .#",
+    "#.                             ......      ..                  .#",
+    "#.                         .@$$$####$%$#@.+&$                  .#",
+    "#.                       .#$$#+.     +#$%%%%$                  .#",
+    "#.                      .$$#$          .#$$%$                  .#",
+    "#. .............    ....$$$$$          ++$$%$+@@@@@@@@@@@@@@@  .#",
+    "#. ##$$$$$$%%%%@    %%&&&&%%$@         %&@#$$@@$%%&&&%%%&&&&&  .#",
+    "#.   +$$$$$%@         .&%####%$@       $&$.$#  #$%%%&    @&%&  .#",
+    "#.    +$$$$$%         +&$###$%%&&$@.   $&.     #$%%%&.    .%&  .#",
+    "#.     @$$$$%$        %##$##$%&&&&&&%#.%#      #$$%%&.     @&  .#",
+    "#.      $$$$$%+      #&  #$$%%&&&&&&&%%$$@     #$$%%&.      +  .#",
+    "#.      .$$$$$%     +&+   .#%&&&&&&&&%$$#$$#   #$$%%&.         .#",
+    "#.       @$$$$%$    %$       @%&&&&&&%$$###$$  #$$%%&.         .#",
+    "#.        #$$$%%@  #&  .        +$&&&%$####$%$ #$$%%&.         .#",
+    "#.         $$$%%% .&@ +%#          .@$$$###$$% #$$$%&.         .#",
+    "#.         +%$%%%$$%  +$$+             #$#$$$% @$$$%&.         .#",
+    "#.          #%%%%%&.  +%$$              ##$$%$ @$$$%%.         .#",
+    "#.           $$%%%@   +%$$$.            #$$$%. @$$$$%.         .#",
+    "#.           +%%%$    +%$$#$@          +$$%$   @#$$$%+         .#",
+    "#.            @%%.    +%%%$$$$#@++.++@#$$$@ @@##$$$%%%$$@      .#",
+    "#.             #@     +&#  .@@###$$$###@.   @+++@@@@###$@      .#",
+    "#.                                                             .#",
+    "#.                                                             .#",
+    "#.                                                             .#",
+    "#.                                                             .#",
+    "#.                                                             .#",
+    "#.                   .@$$$$$$$$  .$%%%%%%#                     .#",
+    "#.                  .......      .@@@@@@@@@.                   .#",
+    "#.                 ........   @@@+@@@@@@@@@@+                  .#",
+    "@#                .........  .####@@@@@@@@@@@+                 #@",
+    " @$$$$$$$$$$$$$$$..........  .@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$@ ",
+    "                  .........  .@@@@@@@@@@@@@@@.                   ",
+    "                   ........       @@@@@@@@@@.                    ",
+    "                    ...........  .@@@@@@@@@                      ",
+    "                     ..........  .@@@@@@@@                       "};
+
+const char* fxRemoveOff_xpm[] = {
+    "18 18 9 1",
+    "  c None",
+    ". c #242623",
+    "+ c #2F312E",
+    "@ c #393A38",
+    "# c #484A47",
+    "$ c #5D5F5C",
+    "% c #8E908D",
+    "& c #9B9D9A",
+    "* c #BDBFBC",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    ".....+#@..@#+.....",
+    "......&*++*&......",
+    "......@*%%*@......",
+    ".......$**$.......",
+    ".......#**#.......",
+    "......+*&&*+......",
+    "......%*@@*%......",
+    "......@@..@@......",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    ".................."};
+
+const char* fxRemoveOn_xpm[] = {
+    "18 18 9 1",
+    "  c None",
+    ". c #4D4F4C",
+    "+ c #575956",
+    "@ c #5C5D5B",
+    "# c #666865",
+    "$ c #787977",
+    "% c #9C9E9B",
+    "& c #A6A8A5",
+    "* c #BFC1BE",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "......#@..@#......",
+    "......&*++*&......",
+    "......@*%%*@......",
+    ".......$**$.......",
+    ".......#**#.......",
+    "......+*&&*+......",
+    "......%*@+*%......",
+    "......@+..+@......",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    ".................."};
+#endif // #ifdef WITH_VST
+
+const char* divideOn_xpm[] = {
+    "18 18 7 1",
+    "  c #5A5A5A",
+    ". c #696969",
+    "+ c #757575",
+    "@ c #8B8B8B",
+    "# c #AAAAAA",
+    "$ c #BBBBBB",
+    "% c #BDBDBD",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "        @@        ",
+    "        %$        ",
+    "        ++        ",
+    "    .########.    ",
+    "    .########.    ",
+    "        ++        ",
+    "        %$        ",
+    "        @@        ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* divideOff_xpm[] = {
+    "18 18 8 1",
+    "  c #252525",
+    ". c #3B3B3B",
+    "+ c #4D4D4D",
+    "@ c #6D6D6D",
+    "# c #6E6E6E",
+    "$ c #9C9C9C",
+    "% c #B5B5B5",
+    "& c #B7B7B7",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "        @#        ",
+    "        &%        ",
+    "        ++        ",
+    "    .$$$$$$$$.    ",
+    "    .$$$$$$$$.    ",
+    "        ++        ",
+    "        &%        ",
+    "        @#        ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* multiplyOn_xpm[] = {
+    "18 18 8 1",
+    "  c #595B58",
+    ". c #737572",
+    "+ c #747673",
+    "@ c #8B8D8A",
+    "# c #8D8F8C",
+    "$ c #8E908D",
+    "% c #8F918E",
+    "& c #C7C9C6",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "      +    .      ",
+    "     +&$  #&.     ",
+    "      #&$#&#      ",
+    "       @&&#       ",
+    "       @&&%       ",
+    "      @&#@&%      ",
+    "     +&#  #&+     ",
+    "      +    .      ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* multiplyOff_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #4A4C49",
+    "+ c #4D4E4C",
+    "@ c #6D6F6C",
+    "# c #717370",
+    "$ c #737572",
+    "% c #757774",
+    "& c #C7C9C6",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "      +    .      ",
+    "     +&$  #&.     ",
+    "      #&$#&#      ",
+    "       @&&#       ",
+    "       @&&%       ",
+    "      @&$@&%      ",
+    "     +&#  #&+     ",
+    "      +    .      ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* channelStop_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #312D2C",
+    "+ c #413A3A",
+    "@ c #615253",
+    "# c #73605F",
+    "$ c #7A6663",
+    "% c #9C7E7D",
+    "& c #B08D8E",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "     ##.          ",
+    "     $&%@         ",
+    "     $&&&%+       ",
+    "     $&&&&&$.     ",
+    "     $&&&&&&&@    ",
+    "     $&&&&&&&@.   ",
+    "     $&&&&&$.     ",
+    "     $&&&%+       ",
+    "     $&&@         ",
+    "     $#.          ",
+    "     .            ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* channelPlay_xpm[] = {
+    "18 18 8 1",
+    "  c #4D4F4C",
+    ". c #554E56",
+    "+ c #5A4D59",
+    "@ c #605068",
+    "# c #775086",
+    "$ c #8A509C",
+    "% c #9E50B5",
+    "& c #AD52D0",
+    "                  ",
+    "                  ",
+    "                  ",
+    "     .            ",
+    "     $$.          ",
+    "     $&%#         ",
+    "     $&&&%@       ",
+    "     $&&&&&$.     ",
+    "     $&&&&&&&#.   ",
+    "     $&&&&&&&#.   ",
+    "     $&&&&&$+     ",
+    "     $&&&%@       ",
+    "     $&&#         ",
+    "     $$.          ",
+    "     .            ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* armOff_xpm[] = {
+    "18 18 8 1",
+    "  c #242523",
+    ". c #4F4445",
+    "+ c #514647",
+    "@ c #6D5C5E",
+    "# c #8E7372",
+    "$ c #AA8889",
+    "% c #AC898A",
+    "& c #B18E8F",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "      +#%%#.      ",
+    "     @&&&&&&@     ",
+    "    +&&&&&&&&.    ",
+    "    #&&&&&&&&#    ",
+    "    %&&&&&&&&%    ",
+    "    %&&&&&&&&%    ",
+    "    #&&&&&&&&#    ",
+    "    .&&&&&&&&.    ",
+    "     @&&&&&&@     ",
+    "      .#%%#.      ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* armOn_xpm[] = {
+    "18 18 8 1",
+    "  c #4D4F4C",
+    ". c #6B5077",
+    "+ c #805191",
+    "@ c #9950AD",
+    "# c #9751B3",
+    "$ c #9553AD",
+    "% c #AA52C9",
+    "& c #AE52D1",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "      .#%%#.      ",
+    "     +&&&&&&+     ",
+    "    .&&&&&&&&.    ",
+    "    #&&&&&&&&@    ",
+    "    %&&&&&&&&%    ",
+    "    %&&&&&&&&%    ",
+    "    #&&&&&&&&$    ",
+    "    .&&&&&&&&.    ",
+    "     +&&&&&&+     ",
+    "      .@%%$.      ",
+    "                  ",
+    "                  ",
+    "                  ",
+    "                  "};
+
+const char* armDisabled_xpm[] = {
+    "18 18 7 1",
+    "  c None",
+    ". c #232523",
+    "+ c #303230",
+    "@ c #393B38",
+    "# c #424441",
+    "$ c #4B4D4A",
+    "% c #4D4F4C",
+    "..................",
+    "..................",
+    "..................",
+    "..................",
+    "......+#$$#+......",
+    ".....@%%%%%%@.....",
+    "....+%%%%%%%%+....",
+    "....#%%%%%%%%#....",
+    "....$%%%%%%%%$....",
+    "....$%%%%%%%%$....",
+    "....#%%%%%%%%#....",
+    "....+%%%%%%%%+....",
+    ".....@%%%%%%@.....",
+    "......+#$$#+......",
+    "..................",
+    "..................",
+    "..................",
+    ".................."};
\ No newline at end of file
diff --git a/src/core/graphics.h b/src/core/graphics.h
new file mode 100644 (file)
index 0000000..7e18e5f
--- /dev/null
@@ -0,0 +1,117 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * graphics
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GRAPHICS_H
+#define G_GRAPHICS_H
+
+extern const char* giada_logo_xpm[];
+
+extern const char* loopRepeat_xpm[];
+extern const char* loopBasic_xpm[];
+extern const char* loopOnce_xpm[];
+extern const char* loopOnceBar_xpm[];
+extern const char* oneshotBasic_xpm[];
+extern const char* oneshotBasicPause_xpm[];
+extern const char* oneshotRetrig_xpm[];
+extern const char* oneshotPress_xpm[];
+extern const char* oneshotEndless_xpm[];
+
+extern const char* updirOff_xpm[];
+extern const char* updirOn_xpm[];
+
+extern const char* pause_xpm[];
+extern const char* play_xpm[];
+
+extern const char* zoomInOff_xpm[];
+extern const char* zoomInOn_xpm[];
+extern const char* zoomOutOff_xpm[];
+extern const char* zoomOutOn_xpm[];
+
+extern const char* scrollLeftOff_xpm[];
+extern const char* scrollLeftOn_xpm[];
+extern const char* scrollRightOff_xpm[];
+extern const char* scrollRightOn_xpm[];
+
+extern const char* rewindOff_xpm[];
+extern const char* rewindOn_xpm[];
+
+extern const char* recOff_xpm[];
+extern const char* recOn_xpm[];
+
+extern const char* metronomeOff_xpm[];
+extern const char* metronomeOn_xpm[];
+
+extern const char* recTriggerModeOff_xpm[];
+extern const char* recTriggerModeOn_xpm[];
+
+extern const char* inputRecOn_xpm[];
+extern const char* inputRecOff_xpm[];
+
+extern const char* freeInputRecOn_xpm[];
+extern const char* freeInputRecOff_xpm[];
+
+extern const char* divideOn_xpm[];
+extern const char* divideOff_xpm[];
+extern const char* multiplyOn_xpm[];
+extern const char* multiplyOff_xpm[];
+
+extern const char* muteOff_xpm[];
+extern const char* muteOn_xpm[];
+
+extern const char* soloOff_xpm[];
+extern const char* soloOn_xpm[];
+
+extern const char* armOff_xpm[];
+extern const char* armOn_xpm[];
+extern const char* armDisabled_xpm[];
+
+extern const char* readActionOn_xpm[];
+extern const char* readActionOff_xpm[];
+extern const char* readActionDisabled_xpm[];
+
+extern const char* channelStop_xpm[];
+extern const char* channelPlay_xpm[];
+
+#ifdef WITH_VST
+extern const char* fxOff_xpm[];
+extern const char* fxOn_xpm[];
+
+extern const char* fxShiftUpOn_xpm[];
+extern const char* fxShiftUpOff_xpm[];
+extern const char* fxShiftDownOn_xpm[];
+extern const char* fxShiftDownOff_xpm[];
+
+extern const char* fxRemoveOff_xpm[];
+extern const char* fxRemoveOn_xpm[];
+
+extern const char* vstLogo_xpm[];
+#endif
+
+extern const char* giada_icon[];
+
+#endif
diff --git a/src/core/idManager.cpp b/src/core/idManager.cpp
new file mode 100644 (file)
index 0000000..87f62bb
--- /dev/null
@@ -0,0 +1,69 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "idManager.h"
+
+namespace giada::m
+{
+IdManager::IdManager()
+: m_id(0)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void IdManager::set(ID id)
+{
+       if (id != 0 && id > m_id)
+               m_id = id;
+}
+
+/* -------------------------------------------------------------------------- */
+
+ID IdManager::generate(ID id)
+{
+       if (id != 0)
+       {
+               m_id = id;
+               return id;
+       }
+       return ++m_id;
+}
+
+/* -------------------------------------------------------------------------- */
+
+ID IdManager::get() const
+{
+       return m_id;
+}
+
+/* -------------------------------------------------------------------------- */
+
+ID IdManager::getNext() const
+{
+       return m_id + 1;
+}
+} // namespace giada::m
diff --git a/src/core/idManager.h b/src/core/idManager.h
new file mode 100644 (file)
index 0000000..32bf1ae
--- /dev/null
@@ -0,0 +1,67 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_ID_MANAGER_H
+#define G_ID_MANAGER_H
+
+#include "core/types.h"
+
+namespace giada::m
+{
+class IdManager
+{
+public:
+       IdManager();
+
+       /* set
+       Stores a new id, only if != 0 (valid) and greater than current id (unique). */
+
+       void set(ID id);
+
+       /* generate
+       Generates a new unique id. If 'id' parameter is passed in is valid, it just 
+       returns it with no unique id generation. Useful when loading things from the 
+       model that already have their own id. */
+
+       ID generate(ID id = 0);
+
+       /* get
+       Returns the current id, a.k.a. the last generated one. */
+
+       ID get() const;
+
+       /* getNext
+       Returns the upcoming ID, that is the ID that will be generated on the next
+       call. */
+
+       ID getNext() const;
+
+private:
+       ID m_id;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/init.cpp b/src/core/init.cpp
new file mode 100644 (file)
index 0000000..ccd97f0
--- /dev/null
@@ -0,0 +1,140 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef __APPLE__
+#include <pwd.h>
+#endif
+#include "core/engine.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/ui.h"
+#include "gui/updater.h"
+#include "utils/log.h"
+#include "utils/ver.h"
+#ifdef WITH_TESTS
+#define CATCH_CONFIG_RUNNER
+#include "tests/actionRecorder.cpp"
+#include "tests/midiLighter.cpp"
+#include "tests/samplePlayer.cpp"
+#include "tests/utils.cpp"
+#include "tests/wave.cpp"
+#include "tests/waveFx.cpp"
+#include "tests/waveManager.cpp"
+#include "tests/waveReader.cpp"
+#include <catch2/catch.hpp>
+#include <string>
+#include <vector>
+#endif
+#include <FL/Fl.H>
+
+extern giada::m::Engine g_engine;
+extern giada::v::Ui     g_ui;
+
+namespace giada::m::init
+{
+int tests(int argc, char** argv)
+{
+#ifdef WITH_TESTS
+       std::vector<char*> args(argv, argv + argc);
+       if (args.size() > 1 && strcmp(args[1], "--run-tests") == 0)
+               return Catch::Session().run(args.size() - 1, &args[1]);
+       else
+               return -1;
+#else
+       (void)argc;
+       (void)argv;
+       return -1;
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+void printBuildInfo()
+{
+       u::log::print("[init] Giada %s\n", G_VERSION_STR);
+       u::log::print("[init] Build date: " BUILD_DATE "\n");
+#ifdef G_DEBUG_MODE
+       u::log::print("[init] Debug build\n");
+#else
+       u::log::print("[init] Release build\n");
+#endif
+       u::log::print("[init] Dependencies:\n");
+       u::log::print("[init]   FLTK - %d.%d.%d\n", FL_MAJOR_VERSION, FL_MINOR_VERSION, FL_PATCH_VERSION);
+       u::log::print("[init]   RtAudio - %s\n", u::ver::getRtAudioVersion());
+       u::log::print("[init]   RtMidi - %s\n", u::ver::getRtMidiVersion());
+       u::log::print("[init]   Libsamplerate\n"); // TODO - print version
+       u::log::print("[init]   Libsndfile - %s\n", u::ver::getLibsndfileVersion());
+       u::log::print("[init]   JSON for modern C++ - %d.%d.%d\n",
+           NLOHMANN_JSON_VERSION_MAJOR, NLOHMANN_JSON_VERSION_MINOR, NLOHMANN_JSON_VERSION_PATCH);
+#ifdef WITH_VST
+       u::log::print("[init]   JUCE - %d.%d.%d\n", JUCE_MAJOR_VERSION, JUCE_MINOR_VERSION, JUCE_BUILDNUMBER);
+#endif
+       KernelAudio::logCompiledAPIs();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void startup(int argc, char** argv)
+{
+#ifdef WITH_VST
+       juce::initialiseJuce_GUI();
+#endif
+       g_engine.init();
+       g_ui.init(argc, argv, g_engine);
+
+       if (!g_engine.kernelAudio.isReady())
+               v::gdAlert("Your soundcard isn't configured correctly.\n"
+                          "Check the configuration and restart Giada.");
+}
+
+/* -------------------------------------------------------------------------- */
+
+int run()
+{
+       Fl::lock(); // Enable multithreading in FLTK
+       return Fl::run();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void closeMainWindow()
+{
+       if (!v::gdConfirmWin("Warning", "Quit Giada: are you sure?"))
+               return;
+       shutdown();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void shutdown()
+{
+       g_ui.shutdown();
+       g_engine.shutdown();
+#ifdef WITH_VST
+       juce::shutdownJuce_GUI();
+#endif
+       u::log::print("[init] Giada %s closed\n\n", G_VERSION_STR);
+}
+} // namespace giada::m::init
diff --git a/src/core/init.h b/src/core/init.h
new file mode 100644 (file)
index 0000000..1c587e2
--- /dev/null
@@ -0,0 +1,45 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_INIT_H
+#define G_INIT_H
+
+namespace giada::m::init
+{
+/* tests
+Performs tests, if requested. Returns -1 if no tests are available or the 
+`--run-tests` has not been passed in. */
+
+int tests(int argc, char** argv);
+
+void printBuildInfo();
+void startup(int argc, char** argv);
+int  run();
+void closeMainWindow();
+void shutdown();
+} // namespace giada::m::init
+
+#endif
\ No newline at end of file
diff --git a/src/core/jackTransport.cpp b/src/core/jackTransport.cpp
new file mode 100644 (file)
index 0000000..9ebcac6
--- /dev/null
@@ -0,0 +1,147 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "jackTransport.h"
+#ifdef WITH_AUDIO_JACK
+#include <jack/intclient.h>
+#include <jack/transport.h>
+#endif
+
+namespace giada::m
+{
+bool JackTransport::State::operator!=(const State& o) const
+{
+       return !(running == o.running && bpm == o.bpm && frame == o.frame);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+JackTransport::JackTransport()
+#ifdef WITH_AUDIO_JACK
+: m_jackHandle(nullptr)
+#endif
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool JackTransport::start() const
+{
+#ifdef WITH_AUDIO_JACK
+       if (m_jackHandle == nullptr)
+               return false;
+       jack_transport_start(m_jackHandle);
+       return true;
+#else
+       return false;
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool JackTransport::stop() const
+{
+#ifdef WITH_AUDIO_JACK
+       if (m_jackHandle == nullptr)
+               return false;
+       jack_transport_stop(m_jackHandle);
+       return true;
+#else
+       return false;
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool JackTransport::setPosition(uint32_t frame) const
+{
+#ifdef WITH_AUDIO_JACK
+       if (m_jackHandle == nullptr)
+               return false;
+       jack_position_t position;
+       jack_transport_query(m_jackHandle, &position);
+       position.frame = frame;
+       jack_transport_reposition(m_jackHandle, &position);
+       return true;
+#else
+       (void)frame;
+       return false;
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool JackTransport::setBpm(double bpm) const
+{
+#ifdef WITH_AUDIO_JACK
+       if (m_jackHandle == nullptr)
+               return false;
+       jack_position_t position;
+       jack_transport_query(m_jackHandle, &position);
+       position.valid            = jack_position_bits_t::JackPositionBBT;
+       position.bar              = 0; // no such info from Giada
+       position.beat             = 0; // no such info from Giada
+       position.tick             = 0; // no such info from Giada
+       position.beats_per_minute = bpm;
+       jack_transport_reposition(m_jackHandle, &position);
+       return true;
+#else
+       (void)bpm;
+       return false;
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+JackTransport::State JackTransport::getState() const
+{
+#ifdef WITH_AUDIO_JACK
+       if (m_jackHandle == nullptr)
+               return {};
+
+       jack_position_t        position;
+       jack_transport_state_t ts = jack_transport_query(m_jackHandle, &position);
+
+       return {
+           ts != JackTransportStopped,
+           position.beats_per_minute,
+           position.frame};
+#else
+       return {};
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_AUDIO_JACK
+void JackTransport::setHandle(jack_client_t* h)
+{
+       m_jackHandle = h;
+}
+#endif
+} // namespace giada::m
diff --git a/src/core/jackTransport.h b/src/core/jackTransport.h
new file mode 100644 (file)
index 0000000..b733474
--- /dev/null
@@ -0,0 +1,74 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_JACK_TRANSPORT_H
+#define G_JACK_TRANSPORT_H
+
+#ifdef WITH_AUDIO_JACK
+#include <jack/jack.h>
+#endif
+#include <cstdint>
+
+namespace giada::m
+{
+class JackTransport final
+{
+public:
+       struct State
+       {
+               bool     running;
+               double   bpm;
+               uint32_t frame;
+
+               bool operator!=(const State& o) const;
+       };
+
+       JackTransport();
+
+       bool  start() const;
+       bool  stop() const;
+       bool  setPosition(uint32_t frame) const;
+       bool  setBpm(double bpm) const;
+       State getState() const;
+
+#ifdef WITH_AUDIO_JACK
+       void setHandle(jack_client_t*);
+#endif
+
+private:
+       /* m_jackHandle
+       Optional handle to JACK. If nullptr the JackTransport class is not
+       initialized and all public transport methods above will return false. This
+       is useful when you are on a platform that supports JACK (e.g. Linux) but
+       the JACK API is currently not selected. */
+
+#ifdef WITH_AUDIO_JACK
+       jack_client_t* m_jackHandle;
+#endif
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/kernelAudio.cpp b/src/core/kernelAudio.cpp
new file mode 100644 (file)
index 0000000..4fa059f
--- /dev/null
@@ -0,0 +1,390 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * KernelAudio
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/kernelAudio.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "utils/log.h"
+#include "utils/vector.h"
+#include <cassert>
+#include <cstddef>
+
+namespace giada::m
+{
+KernelAudio::KernelAudio()
+: onAudioCallback(nullptr)
+, m_ready(false)
+, m_inputEnabled(false)
+, m_realBufferSize(0)
+, m_realSampleRate(0)
+, m_channelsOutCount(0)
+, m_channelsInCount(0)
+, m_api(0)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+int KernelAudio::openDevice(const Conf::Data& conf)
+{
+       assert(onAudioCallback != nullptr);
+
+       m_api = conf.soundSystem;
+       u::log::print("[KA] using system 0x%x\n", m_api);
+
+#if defined(__linux__) || defined(__FreeBSD__)
+
+       if (m_api == G_SYS_API_JACK && hasAPI(RtAudio::UNIX_JACK))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::UNIX_JACK);
+       else if (m_api == G_SYS_API_ALSA && hasAPI(RtAudio::LINUX_ALSA))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::LINUX_ALSA);
+       else if (m_api == G_SYS_API_PULSE && hasAPI(RtAudio::LINUX_PULSE))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::LINUX_PULSE);
+
+#elif defined(__FreeBSD__)
+
+       if (m_api == G_SYS_API_JACK && hasAPI(RtAudio::UNIX_JACK))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::UNIX_JACK);
+       else if (m_api == G_SYS_API_PULSE && hasAPI(RtAudio::LINUX_PULSE))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::LINUX_PULSE);
+
+#elif defined(_WIN32)
+
+       if (m_api == G_SYS_API_DS && hasAPI(RtAudio::WINDOWS_DS))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::WINDOWS_DS);
+       else if (m_api == G_SYS_API_ASIO && hasAPI(RtAudio::WINDOWS_ASIO))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::WINDOWS_ASIO);
+       else if (m_api == G_SYS_API_WASAPI && hasAPI(RtAudio::WINDOWS_WASAPI))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::WINDOWS_WASAPI);
+
+#elif defined(__APPLE__)
+
+       if (m_api == G_SYS_API_CORE && hasAPI(RtAudio::MACOSX_CORE))
+               m_rtAudio = std::make_unique<RtAudio>(RtAudio::MACOSX_CORE);
+
+#endif
+
+       else
+       {
+               u::log::print("[KA] No API available, nothing to do!\n");
+               return 0;
+       }
+
+       m_rtAudio->setErrorCallback([](RtAudioErrorType type, const std::string& msg) {
+               u::log::print("[KA] RtAudio error %d: %s\n", type, msg.c_str());
+       });
+
+       u::log::print("[KA] Opening device out=%d, in=%d, samplerate=%d\n",
+           conf.soundDeviceOut, conf.soundDeviceIn, conf.samplerate);
+
+       m_devices = fetchDevices();
+       printDevices(m_devices);
+
+       /* Abort here if devices found are zero. */
+
+       if (m_devices.size() == 0)
+       {
+               closeDevice();
+               return 0;
+       }
+
+       RtAudio::StreamParameters outParams;
+       RtAudio::StreamParameters inParams;
+
+       outParams.deviceId     = conf.soundDeviceOut == G_DEFAULT_SOUNDDEV_OUT ? m_rtAudio->getDefaultOutputDevice() : conf.soundDeviceOut;
+       outParams.nChannels    = conf.channelsOutCount;
+       outParams.firstChannel = conf.channelsOutStart;
+
+       /* Input device can be disabled. Unlike the output, here we are using all
+       channels and let the user choose which one to record from in the configuration
+       panel. */
+
+       if (conf.soundDeviceIn != -1)
+       {
+               inParams.deviceId     = conf.soundDeviceIn;
+               inParams.nChannels    = conf.channelsInCount;
+               inParams.firstChannel = conf.channelsInStart;
+               m_inputEnabled        = true;
+       }
+       else
+               m_inputEnabled = false;
+
+       RtAudio::StreamOptions options;
+       options.streamName      = G_APP_NAME;
+       options.numberOfBuffers = 4; // TODO - wtf?
+
+       m_realBufferSize   = conf.buffersize;
+       m_realSampleRate   = conf.samplerate;
+       m_channelsOutCount = conf.channelsOutCount;
+       m_channelsInCount  = conf.channelsInCount;
+
+#ifdef WITH_AUDIO_JACK
+
+       /* If JACK, use its own sample rate, not the one coming from the conf
+       object. */
+
+       if (m_api == G_SYS_API_JACK)
+       {
+               assert(m_devices.size() > 0);
+               assert(m_devices[0].sampleRates.size() > 0);
+
+               m_realSampleRate = m_devices[0].sampleRates[0];
+               u::log::print("[KA] JACK in use, samplerate=%d\n", m_realSampleRate);
+       }
+
+#endif
+
+       m_callbackInfo = {
+           /* kernelAudio      = */ this,
+           /* ready            = */ true,
+           /* withJack         = */ getAPI() == G_SYS_API_JACK,
+           /* outBuf           = */ nullptr, // filled later on in audio callback
+           /* inBuf            = */ nullptr, // filled later on in audio callback
+           /* bufferSize       = */ 0,       // filled later on in audio callback
+           /* channelsOutCount = */ m_channelsOutCount,
+           /* channelsInCount  = */ m_channelsInCount};
+
+       RtAudioErrorType res = m_rtAudio->openStream(
+           &outParams,                                     // output params
+           conf.soundDeviceIn != -1 ? &inParams : nullptr, // input params if inDevice is selected
+           RTAUDIO_FLOAT32,                                // audio format
+           m_realSampleRate,                               // sample rate
+           &m_realBufferSize,                              // buffer size in byte
+           &audioCallback,                                 // audio callback
+           &m_callbackInfo,                                // user data passed to callback
+           &options);
+
+       if (res == RtAudioErrorType::RTAUDIO_NO_ERROR)
+       {
+               m_ready = true;
+               return 1;
+       }
+       else
+       {
+               closeDevice();
+               return 0;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+int KernelAudio::startStream()
+{
+       if (m_rtAudio->startStream() == RtAudioErrorType::RTAUDIO_NO_ERROR)
+       {
+               u::log::print("[KA] Start stream - latency = %lu\n", m_rtAudio->getStreamLatency());
+               return 1;
+       }
+       return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int KernelAudio::stopStream()
+{
+       if (m_rtAudio->stopStream() == RtAudioErrorType::RTAUDIO_NO_ERROR)
+       {
+               u::log::print("[KA] Stop stream\n");
+               return 1;
+       }
+       return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void KernelAudio::closeDevice()
+{
+       if (!m_rtAudio->isStreamOpen())
+               return;
+       m_rtAudio->stopStream();
+       m_rtAudio->closeStream();
+       m_rtAudio.reset(nullptr);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool KernelAudio::isReady() const
+{
+       return m_ready;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int  KernelAudio::getBufferSize() const { return static_cast<int>(m_realBufferSize); }
+int  KernelAudio::getSampleRate() const { return m_realSampleRate; }
+int  KernelAudio::getChannelsOutCount() const { return m_channelsOutCount; }
+int  KernelAudio::getChannelsInCount() const { return m_channelsInCount; }
+bool KernelAudio::isInputEnabled() const { return m_inputEnabled; }
+
+/* -------------------------------------------------------------------------- */
+
+m::KernelAudio::Device KernelAudio::getDevice(const char* name) const
+{
+       for (Device device : m_devices)
+               if (name == device.name)
+                       return device;
+       return {0, false};
+}
+
+/* -------------------------------------------------------------------------- */
+
+const std::vector<m::KernelAudio::Device>& KernelAudio::getDevices() const
+{
+       return m_devices;
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_AUDIO_JACK
+jack_client_t* KernelAudio::getJackHandle() const
+{
+       return static_cast<jack_client_t*>(m_rtAudio->HACK__getJackClient());
+}
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+bool KernelAudio::hasAPI(int API) const
+{
+       std::vector<RtAudio::Api> APIs;
+       RtAudio::getCompiledApi(APIs);
+       for (unsigned i = 0; i < APIs.size(); i++)
+               if (APIs.at(i) == API)
+                       return true;
+       return false;
+}
+
+int KernelAudio::getAPI() const { return m_api; }
+
+/* -------------------------------------------------------------------------- */
+
+void KernelAudio::logCompiledAPIs()
+{
+       std::vector<RtAudio::Api> APIs;
+       RtAudio::getCompiledApi(APIs);
+
+       u::log::print("[KA] Compiled RtAudio APIs: %d\n", APIs.size());
+
+       for (const RtAudio::Api& m_api : APIs)
+       {
+               switch (m_api)
+               {
+               case RtAudio::Api::LINUX_ALSA:
+                       u::log::print("  ALSA\n");
+                       break;
+               case RtAudio::Api::LINUX_PULSE:
+                       u::log::print("  PulseAudio\n");
+                       break;
+               case RtAudio::Api::UNIX_JACK:
+                       u::log::print("  JACK\n");
+                       break;
+               case RtAudio::Api::MACOSX_CORE:
+                       u::log::print("  CoreAudio\n");
+                       break;
+               case RtAudio::Api::WINDOWS_WASAPI:
+                       u::log::print("  WASAPI\n");
+                       break;
+               case RtAudio::Api::WINDOWS_ASIO:
+                       u::log::print("  ASIO\n");
+                       break;
+               case RtAudio::Api::WINDOWS_DS:
+                       u::log::print("  DirectSound\n");
+                       break;
+               case RtAudio::Api::RTAUDIO_DUMMY:
+                       u::log::print("  Dummy\n");
+                       break;
+               default:
+                       u::log::print("  (unknown)\n");
+                       break;
+               }
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+m::KernelAudio::Device KernelAudio::fetchDevice(size_t deviceIndex) const
+{
+       RtAudio::DeviceInfo info = m_rtAudio->getDeviceInfo(deviceIndex);
+
+       if (!info.probed)
+       {
+               u::log::print("[KA] Can't probe device %d\n", deviceIndex);
+               return {deviceIndex};
+       }
+
+       return {
+           deviceIndex,
+           true,
+           info.name,
+           static_cast<int>(info.outputChannels),
+           static_cast<int>(info.inputChannels),
+           static_cast<int>(info.duplexChannels),
+           info.isDefaultOutput,
+           info.isDefaultInput,
+           u::vector::cast<int>(info.sampleRates)};
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<m::KernelAudio::Device> KernelAudio::fetchDevices() const
+{
+       std::vector<Device> out;
+       for (unsigned i = 0; i < m_rtAudio->getDeviceCount(); i++)
+               out.push_back(fetchDevice(i));
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void KernelAudio::printDevices(const std::vector<m::KernelAudio::Device>& devices) const
+{
+       u::log::print("[KA] %d device(s) found\n", devices.size());
+       for (const m::KernelAudio::Device& d : devices)
+       {
+               u::log::print("  %d) %s\n", d.index, d.name);
+               u::log::print("      ins=%d outs=%d duplex=%d\n", d.maxInputChannels, d.maxOutputChannels, d.maxDuplexChannels);
+               u::log::print("      isDefaultOut=%d isDefaultIn=%d\n", d.isDefaultOut, d.isDefaultIn);
+               u::log::print("      sampleRates:\n\t");
+               for (int s : d.sampleRates)
+                       u::log::print("%d ", s);
+               u::log::print("\n");
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+int KernelAudio::audioCallback(void* outBuf, void* inBuf, unsigned bufferSize,
+    double /*streamTime*/, RtAudioStreamStatus /*status*/, void*   data)
+{
+       CallbackInfo info = *static_cast<CallbackInfo*>(data);
+       info.outBuf       = outBuf;
+       info.inBuf        = inBuf;
+       info.bufferSize   = bufferSize;
+       return info.kernelAudio->onAudioCallback(info);
+}
+} // namespace giada::m
diff --git a/src/core/kernelAudio.h b/src/core/kernelAudio.h
new file mode 100644 (file)
index 0000000..e509e91
--- /dev/null
@@ -0,0 +1,122 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_KERNELAUDIO_H
+#define G_KERNELAUDIO_H
+
+#include "core/conf.h"
+#include "deps/rtaudio/RtAudio.h"
+#include <cstddef>
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+#ifdef WITH_AUDIO_JACK
+#include "core/jackTransport.h"
+#endif
+
+namespace giada::m
+{
+class KernelAudio final
+{
+public:
+       struct Device
+       {
+               size_t           index             = 0;
+               bool             probed            = false;
+               std::string      name              = "";
+               int              maxOutputChannels = 0;
+               int              maxInputChannels  = 0;
+               int              maxDuplexChannels = 0;
+               bool             isDefaultOut      = false;
+               bool             isDefaultIn       = false;
+               std::vector<int> sampleRates       = {};
+       };
+
+       struct CallbackInfo
+       {
+               KernelAudio* kernelAudio;
+               bool         ready;
+               bool         withJack;
+               void*        outBuf;
+               void*        inBuf;
+               int          bufferSize;
+               int          channelsOutCount;
+               int          channelsInCount;
+       };
+
+       KernelAudio();
+
+       static void logCompiledAPIs();
+
+       int  openDevice(const Conf::Data& conf);
+       void closeDevice();
+       int  startStream();
+       int  stopStream();
+
+       bool                       isReady() const;
+       bool                       isInputEnabled() const;
+       int                        getBufferSize() const;
+       int                        getSampleRate() const;
+       int                        getChannelsOutCount() const;
+       int                        getChannelsInCount() const;
+       bool                       hasAPI(int API) const;
+       int                        getAPI() const;
+       Device                     getDevice(const char* name) const;
+       const std::vector<Device>& getDevices() const;
+#ifdef WITH_AUDIO_JACK
+       jack_client_t* getJackHandle() const;
+#endif
+
+       /* onAudioCallback
+       Main callback invoked on each audio block. */
+
+       std::function<int(CallbackInfo)> onAudioCallback;
+
+private:
+       static int audioCallback(void*, void*, unsigned, double, RtAudioStreamStatus, void*);
+
+       Device              fetchDevice(size_t deviceIndex) const;
+       std::vector<Device> fetchDevices() const;
+       void                printDevices(const std::vector<Device>& devices) const;
+
+#ifdef WITH_AUDIO_JACK
+       JackTransport m_jackTransport;
+#endif
+       std::vector<Device>      m_devices;
+       std::unique_ptr<RtAudio> m_rtAudio;
+       CallbackInfo             m_callbackInfo;
+       bool                     m_ready;
+       bool                     m_inputEnabled;
+       unsigned                 m_realBufferSize; // Real buffer size from the soundcard
+       int                      m_realSampleRate; // Sample rate might differ if JACK in use
+       int                      m_channelsOutCount;
+       int                      m_channelsInCount;
+       int                      m_api;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/kernelMidi.cpp b/src/core/kernelMidi.cpp
new file mode 100644 (file)
index 0000000..040b0e2
--- /dev/null
@@ -0,0 +1,247 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/kernelMidi.h"
+#include "core/const.h"
+#include "utils/log.h"
+#include <cassert>
+#include <memory>
+
+namespace giada::m
+{
+namespace
+{
+constexpr auto OUTPUT_NAME = "Giada MIDI output";
+constexpr auto INPUT_NAME  = "Giada MIDI input";
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<unsigned char> split_(uint32_t iValue)
+{
+       return {
+           static_cast<unsigned char>((iValue >> 24) & 0xFF),
+           static_cast<unsigned char>((iValue >> 16) & 0xFF),
+           static_cast<unsigned char>((iValue >> 8) & 0xFF)};
+}
+
+/* -------------------------------------------------------------------------- */
+
+uint32_t join_(int b1, int b2, int b3)
+{
+       return (b1 << 24) | (b2 << 16) | (b3 << 8) | (0x00);
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+KernelMidi::KernelMidi()
+: onMidiReceived(nullptr)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool KernelMidi::openOutDevice(int api, int port)
+{
+       if (port == -1)
+               return false;
+
+       u::log::print("[KM] Opening output device '%s', port=%d\n", OUTPUT_NAME, port);
+
+       m_midiOut = makeDevice<RtMidiOut>(api, OUTPUT_NAME);
+       if (m_midiOut == nullptr)
+               return false;
+
+       return openPort(*m_midiOut, port);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool KernelMidi::openInDevice(int api, int port)
+{
+       if (port == -1)
+               return false;
+
+       u::log::print("[KM] Opening input device '%s', port=%d\n", INPUT_NAME, port);
+
+       m_midiIn = makeDevice<RtMidiIn>(api, INPUT_NAME);
+       if (m_midiIn == nullptr)
+               return false;
+
+       if (!openPort(*m_midiIn, port))
+               return false;
+
+       m_midiIn->setCallback(&s_callback, this);
+       m_midiIn->ignoreTypes(true, false, true); // Ignore all system/time msgs, for now
+
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void KernelMidi::logPorts()
+{
+       if (m_midiOut != nullptr)
+               logPorts(*m_midiOut, OUTPUT_NAME);
+       if (m_midiIn != nullptr)
+               logPorts(*m_midiIn, INPUT_NAME);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool KernelMidi::hasAPI(int API) const
+{
+       std::vector<RtMidi::Api> APIs;
+       RtMidi::getCompiledApi(APIs);
+       for (unsigned i = 0; i < APIs.size(); i++)
+               if (APIs.at(i) == API)
+                       return true;
+       return false;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string KernelMidi::getOutPortName(unsigned p) const { return getPortName(*m_midiOut, p); }
+std::string KernelMidi::getInPortName(unsigned p) const { return getPortName(*m_midiIn, p); }
+
+/* -------------------------------------------------------------------------- */
+
+void KernelMidi::send(uint32_t data)
+{
+       if (m_midiOut == nullptr)
+               return;
+
+       std::vector<unsigned char> msg = split_(data);
+
+       m_midiOut->sendMessage(&msg);
+       u::log::print("[KM::send] send msg=0x%X (%X %X %X)\n", data, msg[0], msg[1], msg[2]);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void KernelMidi::send(int b1, int b2, int b3)
+{
+       if (m_midiOut == nullptr)
+               return;
+
+       std::vector<unsigned char> msg(1, b1);
+
+       if (b2 != -1)
+               msg.push_back(b2);
+       if (b3 != -1)
+               msg.push_back(b3);
+
+       m_midiOut->sendMessage(&msg);
+       u::log::print("[KM::send] send msg=(%X %X %X)\n", b1, b2, b3);
+}
+
+/* -------------------------------------------------------------------------- */
+
+unsigned KernelMidi::countOutPorts() const { return m_midiOut != nullptr ? m_midiOut->getPortCount() : 0; }
+unsigned KernelMidi::countInPorts() const { return m_midiIn != nullptr ? m_midiIn->getPortCount() : 0; }
+
+/* -------------------------------------------------------------------------- */
+
+void KernelMidi::s_callback(double /*t*/, std::vector<unsigned char>* msg, void* data)
+{
+       static_cast<KernelMidi*>(data)->callback(msg);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void KernelMidi::callback(std::vector<unsigned char>* msg)
+{
+       assert(onMidiReceived != nullptr);
+
+       if (msg->size() < 3)
+       {
+               G_DEBUG("Received unknown MIDI signal - bytes=" << msg->size());
+               return;
+       }
+
+       onMidiReceived(join_(msg->at(0), msg->at(1), msg->at(2)));
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename Device>
+std::unique_ptr<Device> KernelMidi::makeDevice(int api, std::string name) const
+{
+       try
+       {
+               return std::make_unique<Device>(static_cast<RtMidi::Api>(api), name);
+       }
+       catch (RtMidiError& error)
+       {
+               u::log::print("[KM] Error opening device '%s': %s\n", name.c_str(), error.getMessage());
+               return nullptr;
+       }
+}
+
+template std::unique_ptr<RtMidiOut> KernelMidi::makeDevice(int, std::string) const;
+template std::unique_ptr<RtMidiIn>  KernelMidi::makeDevice(int, std::string) const;
+
+/* -------------------------------------------------------------------------- */
+
+bool KernelMidi::openPort(RtMidi& device, int port)
+{
+       try
+       {
+               device.openPort(port, device.getPortName(port));
+               return true;
+       }
+       catch (RtMidiError& error)
+       {
+               u::log::print("[KM] Error opening port %d: %s\n", port, error.getMessage());
+               return false;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string KernelMidi::getPortName(RtMidi& device, int port) const
+{
+       try
+       {
+               return device.getPortName(port);
+       }
+       catch (RtMidiError& /*error*/)
+       {
+               return "";
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void KernelMidi::logPorts(RtMidi& device, std::string name) const
+{
+       u::log::print("[KM] Device '%s': %d MIDI ports found\n", name.c_str(), device.getPortCount());
+       for (unsigned i = 0; i < device.getPortCount(); i++)
+               u::log::print("  %d) %s\n", i, device.getPortName(i));
+}
+} // namespace giada::m
diff --git a/src/core/kernelMidi.h b/src/core/kernelMidi.h
new file mode 100644 (file)
index 0000000..ac10e22
--- /dev/null
@@ -0,0 +1,90 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_KERNELMIDI_H
+#define G_KERNELMIDI_H
+
+#include "midiMapper.h"
+#include <RtMidi.h>
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string>
+
+namespace giada::m
+{
+class KernelMidi final
+{
+public:
+       KernelMidi();
+
+       unsigned countOutPorts() const;
+       unsigned countInPorts() const;
+
+       /* getOut/InPortName
+    Returns the name of the port 'p'. */
+
+       std::string getOutPortName(unsigned p) const;
+       std::string getInPortName(unsigned p) const;
+
+       bool hasAPI(int API) const;
+
+       /* send
+    Sends a MIDI message 's' as uint32_t or as separate bytes. */
+
+       void send(uint32_t s);
+       void send(int b1, int b2 = -1, int b3 = -1);
+
+       /* setApi
+    Sets the Api in use for both in & out messages. */
+
+       void setApi(int api);
+
+       bool openOutDevice(int api, int port);
+       bool openInDevice(int api, int port);
+
+       void logPorts();
+
+       std::function<void(uint32_t)> onMidiReceived;
+
+private:
+       static void s_callback(double, std::vector<unsigned char>*, void*);
+       void        callback(std::vector<unsigned char>*);
+
+       template <typename Device>
+       std::unique_ptr<Device> makeDevice(int api, std::string name) const;
+
+       std::string getPortName(RtMidi&, int port) const;
+       void        logPorts(RtMidi&, std::string name) const;
+
+       bool openPort(RtMidi&, int port);
+
+       std::unique_ptr<RtMidiOut> m_midiOut;
+       std::unique_ptr<RtMidiIn>  m_midiIn;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/metronome.cpp b/src/core/metronome.cpp
new file mode 100644 (file)
index 0000000..7b5fbb0
--- /dev/null
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "metronome.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+
+namespace giada::m
+{
+void Metronome::trigger(Click c, Frame o)
+{
+       m_rendering = true;
+       m_click     = c;
+       m_offset    = o;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Metronome::render(mcl::AudioBuffer& outBuf)
+{
+       const float* data = m_click == Click::BEAT ? beat : bar;
+       for (Frame f = m_offset; f < outBuf.countFrames() && m_rendering; f++)
+       {
+               for (int c = 0; c < outBuf.countChannels(); c++)
+                       outBuf[f][c] += data[m_tracker];
+               m_tracker = (m_tracker + 1) % CLICK_SIZE;
+               if (m_tracker == 0)
+                       m_rendering = false;
+       }
+       m_offset = 0;
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/metronome.h b/src/core/metronome.h
new file mode 100644 (file)
index 0000000..01bee57
--- /dev/null
@@ -0,0 +1,80 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_METRONOME_H
+#define G_METRONOME_H
+
+#include "core/types.h"
+
+namespace mcl
+{
+class AudioBuffer;
+}
+namespace giada::m
+{
+class Metronome
+{
+public:
+       enum class Click
+       {
+               BEAT,
+               BAR
+       };
+
+       void render(mcl::AudioBuffer& outBuf);
+       void trigger(Click c, Frame o);
+
+       bool running = false;
+
+private:
+       static constexpr Frame CLICK_SIZE = 38;
+
+       static constexpr float beat[CLICK_SIZE] = {
+           0.059033f, 0.117240f, 0.173807f, 0.227943f, 0.278890f, 0.325936f,
+           0.368423f, 0.405755f, 0.437413f, 0.462951f, 0.482013f, 0.494333f,
+           0.499738f, 0.498153f, 0.489598f, 0.474195f, 0.452159f, 0.423798f,
+           0.389509f, 0.349771f, 0.289883f, 0.230617f, 0.173194f, 0.118739f,
+           0.068260f, 0.022631f, -0.017423f, -0.051339f, -0.078721f, -0.099345f,
+           -0.113163f, -0.120295f, -0.121028f, -0.115804f, -0.105209f, -0.089954f,
+           -0.070862f, -0.048844f};
+
+       static constexpr float bar[CLICK_SIZE] = {
+           0.175860f, 0.341914f, 0.488904f, 0.608633f, 0.694426f, 0.741500f,
+           0.747229f, 0.711293f, 0.635697f, 0.524656f, 0.384362f, 0.222636f,
+           0.048496f, -0.128348f, -0.298035f, -0.451105f, -0.579021f, -0.674653f,
+           -0.732667f, -0.749830f, -0.688924f, -0.594091f, -0.474481f, -0.340160f,
+           -0.201360f, -0.067752f, 0.052194f, 0.151746f, 0.226280f, 0.273493f,
+           0.293425f, 0.288307f, 0.262252f, 0.220811f, 0.170435f, 0.117887f,
+           0.069639f, 0.031320f};
+
+       Frame m_tracker   = 0;
+       Frame m_offset    = 0;
+       bool  m_rendering = false;
+       Click m_click     = Click::BEAT;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/midiDispatcher.cpp b/src/core/midiDispatcher.cpp
new file mode 100644 (file)
index 0000000..2cff699
--- /dev/null
@@ -0,0 +1,448 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/midiDispatcher.h"
+#include "core/conf.h"
+#include "core/mixer.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/plugins/plugin.h"
+#include "core/plugins/pluginHost.h"
+#include "core/recorder.h"
+#include "core/types.h"
+#include "glue/events.h"
+#include "glue/plugin.h"
+#include "utils/log.h"
+#include "utils/math.h"
+#include <cassert>
+#include <cstddef>
+#include <vector>
+
+namespace giada::m
+{
+MidiDispatcher::MidiDispatcher(model::Model& m)
+: onDispatch(nullptr)
+, m_learnCb(nullptr)
+, m_model(m)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::startChannelLearn(int param, ID channelId, std::function<void()> f)
+{
+       m_learnCb = [=](m::MidiEvent e) { learnChannel(e, param, channelId, f); };
+}
+
+void MidiDispatcher::startMasterLearn(int param, std::function<void()> f)
+{
+       m_learnCb = [=](m::MidiEvent e) { learnMaster(e, param, f); };
+}
+
+#ifdef WITH_VST
+
+void MidiDispatcher::startPluginLearn(std::size_t paramIndex, ID pluginId, std::function<void()> f)
+{
+       m_learnCb = [=](m::MidiEvent e) { learnPlugin(e, paramIndex, pluginId, f); };
+}
+
+#endif
+
+void MidiDispatcher::stopLearn()
+{
+       m_learnCb = nullptr;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::clearMasterLearn(int param, std::function<void()> f)
+{
+       learnMaster(MidiEvent(), param, f); // Empty event (0x0)
+}
+
+void MidiDispatcher::clearChannelLearn(int param, ID channelId, std::function<void()> f)
+{
+       learnChannel(MidiEvent(), param, channelId, f); // Empty event (0x0)
+}
+
+#ifdef WITH_VST
+
+void MidiDispatcher::clearPluginLearn(std::size_t paramIndex, ID pluginId, std::function<void()> f)
+{
+       learnPlugin(MidiEvent(), paramIndex, pluginId, f); // Empty event (0x0)
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::dispatch(uint32_t msg)
+{
+       assert(onDispatch != nullptr);
+
+       /* Here we want to catch two things: a) note on/note off from a MIDI keyboard 
+       and b) knob/wheel/slider movements from a MIDI controller. 
+       We must also fix the velocity zero issue for those devices that sends NOTE
+       OFF events as NOTE ON + velocity zero. Let's make it a real NOTE OFF event. */
+
+       MidiEvent midiEvent(msg);
+       midiEvent.fixVelocityZero();
+
+       u::log::print("[midiDispatcher] MIDI received - 0x%X (chan %d)\n", midiEvent.getRaw(),
+           midiEvent.getChannel());
+
+       /* Start dispatcher. Don't parse channels if MIDI learn is ON, just learn 
+       the incoming MIDI signal. The action is not invoked directly, but scheduled 
+       to be perfomed by the Event Dispatcher. */
+
+       Action action = {0, 0, 0, midiEvent};
+       auto   event  = m_learnCb != nullptr ? EventDispatcher::EventType::MIDI_DISPATCHER_LEARN : EventDispatcher::EventType::MIDI_DISPATCHER_PROCESS;
+
+       onDispatch(event, action);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::learn(const MidiEvent& e)
+{
+       assert(m_learnCb != nullptr);
+       m_learnCb(e);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::process(const MidiEvent& e)
+{
+       assert(onEventReceived != nullptr);
+
+       processMaster(e);
+       processChannels(e);
+       onEventReceived();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool MidiDispatcher::isMasterMidiInAllowed(int c)
+{
+       int  filter  = m_model.get().midiIn.filter;
+       bool enabled = m_model.get().midiIn.enabled;
+       return enabled && (filter == -1 || filter == c);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool MidiDispatcher::isChannelMidiInAllowed(ID channelId, int c)
+{
+       return m_model.get().getChannel(channelId).midiLearner.isAllowed(c);
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void MidiDispatcher::processPlugins(ID channelId, const std::vector<Plugin*>& plugins,
+    const MidiEvent& midiEvent)
+{
+       uint32_t pure = midiEvent.getRawNoVelocity();
+       float    vf   = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, 1.0f);
+
+       /* Plugins' parameters layout reflects the structure of the matrix
+       Channel::midiInPlugins. It is safe to assume then that Plugin 'p' and 
+       parameter indexes match both the structure of Channel::midiInPlugins and the 
+       vector of plugins. */
+
+       for (Plugin* p : plugins)
+       {
+               for (const MidiLearnParam& param : p->midiInParams)
+               {
+                       if (pure != param.getValue())
+                               continue;
+                       c::events::setPluginParameter(channelId, p->id, param.getIndex(), vf, Thread::MIDI);
+                       u::log::print("  >>> [pluginId=%d paramIndex=%d] (pure=0x%X, value=%d, float=%f)\n",
+                           p->id, param.getIndex(), pure, midiEvent.getVelocity(), vf);
+               }
+       }
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::processChannels(const MidiEvent& midiEvent)
+{
+       uint32_t pure = midiEvent.getRawNoVelocity();
+
+       for (const Channel& c : m_model.get().channels)
+       {
+
+               /* Do nothing on this channel if MIDI in is disabled or filtered out for
+               the current MIDI channel. */
+               if (!c.midiLearner.isAllowed(midiEvent.getChannel()))
+                       continue;
+
+               if (pure == c.midiLearner.keyPress.getValue())
+               {
+                       u::log::print("  >>> keyPress, ch=%d (pure=0x%X)\n", c.id, pure);
+                       c::events::pressChannel(c.id, midiEvent.getVelocity(), Thread::MIDI);
+               }
+               else if (pure == c.midiLearner.keyRelease.getValue())
+               {
+                       u::log::print("  >>> keyRel ch=%d (pure=0x%X)\n", c.id, pure);
+                       c::events::releaseChannel(c.id, Thread::MIDI);
+               }
+               else if (pure == c.midiLearner.mute.getValue())
+               {
+                       u::log::print("  >>> mute ch=%d (pure=0x%X)\n", c.id, pure);
+                       c::events::toggleMuteChannel(c.id, Thread::MIDI);
+               }
+               else if (pure == c.midiLearner.kill.getValue())
+               {
+                       u::log::print("  >>> kill ch=%d (pure=0x%X)\n", c.id, pure);
+                       c::events::killChannel(c.id, Thread::MIDI);
+               }
+               else if (pure == c.midiLearner.arm.getValue())
+               {
+                       u::log::print("  >>> arm ch=%d (pure=0x%X)\n", c.id, pure);
+                       c::events::toggleArmChannel(c.id, Thread::MIDI);
+               }
+               else if (pure == c.midiLearner.solo.getValue())
+               {
+                       u::log::print("  >>> solo ch=%d (pure=0x%X)\n", c.id, pure);
+                       c::events::toggleSoloChannel(c.id, Thread::MIDI);
+               }
+               else if (pure == c.midiLearner.volume.getValue())
+               {
+                       float vf = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, G_MAX_VOLUME);
+                       u::log::print("  >>> volume ch=%d (pure=0x%X, value=%d, float=%f)\n",
+                           c.id, pure, midiEvent.getVelocity(), vf);
+                       c::events::setChannelVolume(c.id, vf, Thread::MIDI);
+               }
+               else if (pure == c.midiLearner.pitch.getValue())
+               {
+                       float vf = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, G_MAX_PITCH);
+                       u::log::print("  >>> pitch ch=%d (pure=0x%X, value=%d, float=%f)\n",
+                           c.id, pure, midiEvent.getVelocity(), vf);
+                       c::events::setChannelPitch(c.id, vf, Thread::MIDI);
+               }
+               else if (pure == c.midiLearner.readActions.getValue())
+               {
+                       u::log::print("  >>> toggle read actions ch=%d (pure=0x%X)\n", c.id, pure);
+                       c::events::toggleReadActionsChannel(c.id, Thread::MIDI);
+               }
+
+#ifdef WITH_VST
+               /* Process learned plugins parameters. */
+               processPlugins(c.id, c.plugins, midiEvent);
+#endif
+
+               /* Redirect raw MIDI message (pure + velocity) to plug-ins in armed
+               channels. */
+               if (c.armed)
+                       c::events::sendMidiToChannel(c.id, midiEvent, Thread::MIDI);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::processMaster(const MidiEvent& midiEvent)
+{
+       const uint32_t       pure   = midiEvent.getRawNoVelocity();
+       const model::MidiIn& midiIn = m_model.get().midiIn;
+
+       if (pure == midiIn.rewind)
+       {
+               c::events::rewindSequencer(Thread::MIDI);
+               u::log::print("  >>> rewind (master) (pure=0x%X)\n", pure);
+       }
+       else if (pure == midiIn.startStop)
+       {
+               c::events::toggleSequencer(Thread::MIDI);
+               u::log::print("  >>> startStop (master) (pure=0x%X)\n", pure);
+       }
+       else if (pure == midiIn.actionRec)
+       {
+               c::events::toggleActionRecording();
+               u::log::print("  >>> actionRec (master) (pure=0x%X)\n", pure);
+       }
+       else if (pure == midiIn.inputRec)
+       {
+               c::events::toggleInputRecording();
+               u::log::print("  >>> inputRec (master) (pure=0x%X)\n", pure);
+       }
+       else if (pure == midiIn.metronome)
+       {
+               c::events::toggleMetronome();
+               u::log::print("  >>> metronome (master) (pure=0x%X)\n", pure);
+       }
+       else if (pure == midiIn.volumeIn)
+       {
+               float vf = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, G_MAX_VOLUME);
+               c::events::setMasterInVolume(vf, Thread::MIDI);
+               u::log::print("  >>> input volume (master) (pure=0x%X, value=%d, float=%f)\n",
+                   pure, midiEvent.getVelocity(), vf);
+       }
+       else if (pure == midiIn.volumeOut)
+       {
+               float vf = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, G_MAX_VOLUME);
+               c::events::setMasterOutVolume(vf, Thread::MIDI);
+               u::log::print("  >>> output volume (master) (pure=0x%X, value=%d, float=%f)\n",
+                   pure, midiEvent.getVelocity(), vf);
+       }
+       else if (pure == midiIn.beatDouble)
+       {
+               c::events::multiplyBeats();
+               u::log::print("  >>> sequencer x2 (master) (pure=0x%X)\n", pure);
+       }
+       else if (pure == midiIn.beatHalf)
+       {
+               c::events::divideBeats();
+               u::log::print("  >>> sequencer /2 (master) (pure=0x%X)\n", pure);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::learnChannel(MidiEvent e, int param, ID channelId, std::function<void()> doneCb)
+{
+       if (!isChannelMidiInAllowed(channelId, e.getChannel()))
+               return;
+
+       uint32_t raw = e.getRawNoVelocity();
+
+       Channel& ch = m_model.get().getChannel(channelId);
+
+       switch (param)
+       {
+       case G_MIDI_IN_KEYPRESS:
+               ch.midiLearner.keyPress.setValue(raw);
+               break;
+       case G_MIDI_IN_KEYREL:
+               ch.midiLearner.keyRelease.setValue(raw);
+               break;
+       case G_MIDI_IN_KILL:
+               ch.midiLearner.kill.setValue(raw);
+               break;
+       case G_MIDI_IN_ARM:
+               ch.midiLearner.arm.setValue(raw);
+               break;
+       case G_MIDI_IN_MUTE:
+               ch.midiLearner.mute.setValue(raw);
+               break;
+       case G_MIDI_IN_SOLO:
+               ch.midiLearner.solo.setValue(raw);
+               break;
+       case G_MIDI_IN_VOLUME:
+               ch.midiLearner.volume.setValue(raw);
+               break;
+       case G_MIDI_IN_PITCH:
+               ch.midiLearner.pitch.setValue(raw);
+               break;
+       case G_MIDI_IN_READ_ACTIONS:
+               ch.midiLearner.readActions.setValue(raw);
+               break;
+       case G_MIDI_OUT_L_PLAYING:
+               ch.midiLighter.playing.setValue(raw);
+               break;
+       case G_MIDI_OUT_L_MUTE:
+               ch.midiLighter.mute.setValue(raw);
+               break;
+       case G_MIDI_OUT_L_SOLO:
+               ch.midiLighter.solo.setValue(raw);
+               break;
+       }
+
+       m_model.swap(model::SwapType::SOFT);
+
+       stopLearn();
+       doneCb();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiDispatcher::learnMaster(MidiEvent e, int param, std::function<void()> doneCb)
+{
+       if (!isMasterMidiInAllowed(e.getChannel()))
+               return;
+
+       uint32_t raw = e.getRawNoVelocity();
+
+       switch (param)
+       {
+       case G_MIDI_IN_REWIND:
+               m_model.get().midiIn.rewind = raw;
+               break;
+       case G_MIDI_IN_START_STOP:
+               m_model.get().midiIn.startStop = raw;
+               break;
+       case G_MIDI_IN_ACTION_REC:
+               m_model.get().midiIn.actionRec = raw;
+               break;
+       case G_MIDI_IN_INPUT_REC:
+               m_model.get().midiIn.inputRec = raw;
+               break;
+       case G_MIDI_IN_METRONOME:
+               m_model.get().midiIn.metronome = raw;
+               break;
+       case G_MIDI_IN_VOLUME_IN:
+               m_model.get().midiIn.volumeIn = raw;
+               break;
+       case G_MIDI_IN_VOLUME_OUT:
+               m_model.get().midiIn.volumeOut = raw;
+               break;
+       case G_MIDI_IN_BEAT_DOUBLE:
+               m_model.get().midiIn.beatDouble = raw;
+               break;
+       case G_MIDI_IN_BEAT_HALF:
+               m_model.get().midiIn.beatHalf = raw;
+               break;
+       }
+
+       m_model.swap(model::SwapType::SOFT);
+
+       stopLearn();
+       doneCb();
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void MidiDispatcher::learnPlugin(MidiEvent e, std::size_t paramIndex, ID pluginId, std::function<void()> doneCb)
+{
+       model::DataLock lock   = m_model.lockData(model::SwapType::NONE);
+       Plugin*         plugin = m_model.findShared<Plugin>(pluginId);
+
+       assert(plugin != nullptr);
+       assert(paramIndex < plugin->midiInParams.size());
+
+       plugin->midiInParams[paramIndex].setValue(e.getRawNoVelocity());
+
+       stopLearn();
+       doneCb();
+}
+
+#endif
+} // namespace giada::m
diff --git a/src/core/midiDispatcher.h b/src/core/midiDispatcher.h
new file mode 100644 (file)
index 0000000..8bef6a4
--- /dev/null
@@ -0,0 +1,109 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MIDI_DISPATCHER_H
+#define G_MIDI_DISPATCHER_H
+
+#include "core/actions/action.h"
+#include "core/midiEvent.h"
+#include "core/model/model.h"
+#include "core/types.h"
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+
+namespace giada::m
+{
+class MidiDispatcher
+{
+public:
+       MidiDispatcher(model::Model&);
+
+       void startChannelLearn(int param, ID channelId, std::function<void()> f);
+       void startMasterLearn(int param, std::function<void()> f);
+       void stopLearn();
+       void clearMasterLearn(int param, std::function<void()> f);
+       void clearChannelLearn(int param, ID channelId, std::function<void()> f);
+#ifdef WITH_VST
+       void startPluginLearn(std::size_t paramIndex, ID pluginId, std::function<void()> f);
+       void clearPluginLearn(std::size_t paramIndex, ID pluginId, std::function<void()> f);
+#endif
+
+       /* dispatch
+    Main callback invoked by kernelMidi whenever a new MIDI data comes in. */
+
+       void dispatch(uint32_t msg);
+
+       /* learn
+    Learns event 'e'. Called by the Event Dispatcher. */
+
+       void learn(const MidiEvent& e);
+
+       /* process
+    Sends event 'e' to channels (masters and keyboard). Called by the Event 
+    Dispatcher. */
+
+       void process(const MidiEvent& e);
+
+       /* onDispatch
+       Callback fired when the dispatch() method is invoked by KernelMidi. */
+
+       std::function<void(EventDispatcher::EventType, Action)> onDispatch;
+
+       /* onEventReceived
+       Callback fired when a MIDI event has been received (passed in by the Event
+       Dispatcher). */
+
+       std::function<void()> onEventReceived;
+
+private:
+       bool isMasterMidiInAllowed(int c);
+       bool isChannelMidiInAllowed(ID channelId, int c);
+
+       void processChannels(const MidiEvent& midiEvent);
+       void processMaster(const MidiEvent& midiEvent);
+
+       void learnChannel(MidiEvent e, int param, ID channelId, std::function<void()> doneCb);
+       void learnMaster(MidiEvent e, int param, std::function<void()> doneCb);
+
+#ifdef WITH_VST
+       void processPlugins(ID channelId, const std::vector<Plugin*>& plugins,
+           const MidiEvent& midiEvent);
+       void learnPlugin(MidiEvent e, std::size_t paramIndex, ID pluginId,
+           std::function<void()> doneCb);
+#endif
+
+       /* cb_midiLearn
+    Callback prepared by the gdMidiGrabber window and called by midiDispatcher. 
+    It contains things to do once the midi message has been stored. */
+
+       std::function<void(MidiEvent)> m_learnCb;
+
+       model::Model& m_model;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/midiEvent.cpp b/src/core/midiEvent.cpp
new file mode 100644 (file)
index 0000000..761106a
--- /dev/null
@@ -0,0 +1,149 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiEvent.h"
+#include "const.h"
+#include "utils/math.h"
+#include <cassert>
+
+namespace giada
+{
+namespace m
+{
+namespace
+{
+constexpr int FLOAT_FACTOR = 10000;
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+MidiEvent::MidiEvent(uint32_t raw, int delta)
+: m_status((raw & 0xF0000000) >> 24)
+, m_channel((raw & 0x0F000000) >> 24)
+, m_note((raw & 0x00FF0000) >> 16)
+, m_velocity((raw & 0x0000FF00) >> 8)
+, m_delta(delta)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* static_cast to avoid ambiguity with MidiEvent(float). */
+MidiEvent::MidiEvent(int byte1, int byte2, int byte3, int delta)
+: MidiEvent(static_cast<uint32_t>((byte1 << 24) | (byte2 << 16) | (byte3 << 8) | (0x00)), delta)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+MidiEvent::MidiEvent(float v, int delta)
+: MidiEvent(ENVELOPE, 0, 0, delta)
+{
+       m_velocity = static_cast<int>(v * FLOAT_FACTOR);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiEvent::setDelta(int d)
+{
+       m_delta = d;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiEvent::setChannel(int c)
+{
+       assert(c >= 0 && c < G_MAX_MIDI_CHANS);
+       m_channel = c;
+}
+
+void MidiEvent::setVelocity(int v)
+{
+       assert(v >= 0 && v <= G_MAX_VELOCITY);
+       m_velocity = v;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MidiEvent::fixVelocityZero()
+{
+       if (m_status == NOTE_ON && m_velocity == 0)
+               m_status = NOTE_OFF;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int MidiEvent::getStatus() const
+{
+       return m_status;
+}
+
+int MidiEvent::getChannel() const
+{
+       return m_channel;
+}
+
+int MidiEvent::getNote() const
+{
+       return m_note;
+}
+
+int MidiEvent::getVelocity() const
+{
+       return m_velocity;
+}
+
+float MidiEvent::getVelocityFloat() const
+{
+       return m_velocity / static_cast<float>(FLOAT_FACTOR);
+}
+
+bool MidiEvent::isNoteOnOff() const
+{
+       return m_status == NOTE_ON || m_status == NOTE_OFF;
+}
+
+int MidiEvent::getDelta() const
+{
+       return m_delta;
+}
+
+/* -------------------------------------------------------------------------- */
+
+uint32_t MidiEvent::getRaw() const
+{
+       return (m_status << 24) | (m_channel << 24) | (m_note << 16) | (m_velocity << 8) | (0x00);
+}
+
+uint32_t MidiEvent::getRawNoVelocity() const
+{
+       return (m_status << 24) | (m_channel << 24) | (m_note << 16) | (0x00 << 8) | (0x00);
+}
+
+} // namespace m
+} // namespace giada
diff --git a/src/core/midiEvent.h b/src/core/midiEvent.h
new file mode 100644 (file)
index 0000000..15ff0e7
--- /dev/null
@@ -0,0 +1,96 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MIDI_EVENT_H
+#define G_MIDI_EVENT_H
+
+#include <cstdint>
+
+namespace giada
+{
+namespace m
+{
+class MidiEvent
+{
+public:
+       static const int NOTE_ON   = 0x90;
+       static const int NOTE_OFF  = 0x80;
+       static const int NOTE_KILL = 0x70;
+       static const int ENVELOPE  = 0xB0;
+
+       /* MidiEvent (1)
+       Creates and empty and invalid MIDI event. */
+
+       MidiEvent() = default;
+
+       MidiEvent(uint32_t raw, int delta = 0);
+       MidiEvent(int byte1, int byte2, int byte3, int delta = 0);
+
+       /* MidiEvent (4)
+       A constructor that takes a float parameter. Useful to build ENVELOPE events 
+       for automations, volume and pitch. */
+
+       MidiEvent(float v, int delta = 0);
+
+       int   getStatus() const;
+       int   getChannel() const;
+       int   getNote() const;
+       int   getVelocity() const;
+       float getVelocityFloat() const;
+       bool  isNoteOnOff() const;
+       int   getDelta() const;
+
+       /* getRaw(), getRawNoVelocity()
+       Returns the raw MIDI message. If getRawNoVelocity(), the velocity value is
+       stripped off (i.e. velocity == 0). */
+
+       uint32_t getRaw() const;
+       uint32_t getRawNoVelocity() const;
+
+       void setDelta(int d);
+       void setChannel(int c);
+       void setVelocity(int v);
+
+       /* fixVelocityZero()
+       According to the MIDI standard, there is a special case if the velocity is 
+       set to zero. The NOTE ON message then has the same meaning as a NOTE OFF 
+       message, switching the note off. Let's fix it. Sometime however you do want
+       a NOTE ON with velocity zero: setting velocity to 0 in MIDI action editor to
+       mute a specific event.  */
+
+       void fixVelocityZero();
+
+private:
+       int m_status;
+       int m_channel;
+       int m_note;
+       int m_velocity;
+       int m_delta;
+};
+} // namespace m
+} // namespace giada
+
+#endif
diff --git a/src/core/midiLearnParam.cpp b/src/core/midiLearnParam.cpp
new file mode 100644 (file)
index 0000000..8166aee
--- /dev/null
@@ -0,0 +1,60 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiLearnParam.h"
+#include <cstddef>
+
+namespace giada::m
+{
+MidiLearnParam::MidiLearnParam()
+: m_param(0)
+, m_index(0)
+{
+}
+
+MidiLearnParam::MidiLearnParam(uint32_t v, std::size_t index)
+: m_param(v)
+, m_index(index)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+uint32_t MidiLearnParam::getValue() const
+{
+       return m_param.load();
+}
+
+void MidiLearnParam::setValue(uint32_t v)
+{
+       m_param.store(v);
+}
+
+std::size_t MidiLearnParam::getIndex() const
+{
+       return m_index;
+}
+} // namespace giada::m
diff --git a/src/core/midiLearnParam.h b/src/core/midiLearnParam.h
new file mode 100644 (file)
index 0000000..bf51f80
--- /dev/null
@@ -0,0 +1,53 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MIDI_LEARN_PARAM_H
+#define G_MIDI_LEARN_PARAM_H
+
+#include "core/weakAtomic.h"
+#include <cstddef>
+#include <atomic>
+
+namespace giada::m
+{
+class MidiLearnParam
+{
+public:
+       MidiLearnParam();
+       MidiLearnParam(uint32_t v, std::size_t index = 0);
+       MidiLearnParam(const MidiLearnParam& o) = default;
+
+       uint32_t    getValue() const;
+       std::size_t getIndex() const;
+       void        setValue(uint32_t v);
+
+  private:
+       WeakAtomic<uint32_t> m_param;
+       std::size_t          m_index;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/midiMapper.cpp b/src/core/midiMapper.cpp
new file mode 100644 (file)
index 0000000..839d69d
--- /dev/null
@@ -0,0 +1,279 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/midiMapper.h"
+#include "core/const.h"
+#include "core/kernelMidi.h"
+#include "core/midiEvent.h"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include <cstddef>
+#include <cstring>
+#include <filesystem>
+#include <fstream>
+#include <string>
+#include <vector>
+#ifdef WITH_TESTS
+#include "tests/mocks/kernelMidiMock.h"
+#endif
+
+namespace nl = nlohmann;
+
+namespace giada::m
+{
+bool MidiMap::isValid() const
+{
+       return !(brand.empty() || device.empty());
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+MidiMapper<KernelMidiI>::MidiMapper(KernelMidiI& k)
+: m_kernelMidi(k)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+const std::vector<std::string>& MidiMapper<KernelMidiI>::getMapFilesFound() const
+{
+       return m_mapFiles;
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+void MidiMapper<KernelMidiI>::init()
+{
+       m_mapsPath = u::fs::getHomePath() + G_SLASH + "midimaps" + G_SLASH;
+
+       /* scan dir of midi maps and load the filenames into m_mapFiles vector. */
+
+       u::log::print("[MidiMapper::init] scanning midimaps directory '%s'...\n",
+           m_mapsPath);
+
+       if (!std::filesystem::exists(m_mapsPath))
+       {
+               u::log::print("[MidiMapper::init] unable to scan midimaps directory!\n");
+               return;
+       }
+
+       for (const auto& d : std::filesystem::directory_iterator(m_mapsPath))
+       {
+               // TODO - check if is a valid midiMap file (verify headers)
+               if (!d.is_regular_file())
+                       continue;
+               u::log::print("[MidiMapper::init] found midiMap '%s'\n", d.path().filename().string());
+               m_mapFiles.push_back(d.path().filename().string());
+       }
+
+       u::log::print("[MidiMapper::init] total midimaps found: %d\n", m_mapFiles.size());
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+int MidiMapper<KernelMidiI>::read(const std::string& file)
+{
+       if (file.empty())
+       {
+               u::log::print("[MidiMapper::read] midiMap not specified, nothing to do\n");
+               return MIDIMAP_NOT_SPECIFIED;
+       }
+
+       u::log::print("[MidiMapper::read] reading midiMap file '%s'\n", file);
+
+       std::ifstream ifs(m_mapsPath + file);
+       if (!ifs.good())
+               return MIDIMAP_UNREADABLE;
+
+       nl::json j = nl::json::parse(ifs);
+
+       currentMap.brand  = j[MIDIMAP_KEY_BRAND];
+       currentMap.device = j[MIDIMAP_KEY_DEVICE];
+
+       if (!readInitCommands(currentMap, j))
+               return MIDIMAP_UNREADABLE;
+       if (readCommand(j, currentMap.muteOn, MIDIMAP_KEY_MUTE_ON))
+               parse(currentMap.muteOn);
+       if (readCommand(j, currentMap.muteOff, MIDIMAP_KEY_MUTE_OFF))
+               parse(currentMap.muteOff);
+       if (readCommand(j, currentMap.soloOn, MIDIMAP_KEY_SOLO_ON))
+               parse(currentMap.soloOn);
+       if (readCommand(j, currentMap.soloOff, MIDIMAP_KEY_SOLO_OFF))
+               parse(currentMap.soloOff);
+       if (readCommand(j, currentMap.waiting, MIDIMAP_KEY_WAITING))
+               parse(currentMap.waiting);
+       if (readCommand(j, currentMap.playing, MIDIMAP_KEY_PLAYING))
+               parse(currentMap.playing);
+       if (readCommand(j, currentMap.stopping, MIDIMAP_KEY_STOPPING))
+               parse(currentMap.stopping);
+       if (readCommand(j, currentMap.stopped, MIDIMAP_KEY_STOPPED))
+               parse(currentMap.stopped);
+       if (readCommand(j, currentMap.playingInaudible, MIDIMAP_KEY_PLAYING_INAUDIBLE))
+               parse(currentMap.playingInaudible);
+
+       return MIDIMAP_READ_OK;
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+bool MidiMapper<KernelMidiI>::isMessageDefined(const MidiMap::Message& m) const
+{
+       return m.offset != -1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+void MidiMapper<KernelMidiI>::sendInitMessages(const MidiMap& midiMap)
+{
+       if (!midiMap.isValid())
+               return;
+
+       for (const MidiMap::Message& m : midiMap.initCommands)
+       {
+               if (m.value == 0x0 || m.channel == -1)
+                       continue;
+               MidiEvent e(m.value);
+               e.setChannel(m.channel);
+               m_kernelMidi.send(e.getRaw());
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+void MidiMapper<KernelMidiI>::sendMidiLightning(uint32_t learnt, const MidiMap::Message& m)
+{
+       // Skip lightning message if not defined in midi map
+
+       if (!isMessageDefined(m))
+       {
+               u::log::print("[MidiMapper::sendMidiLightning] message skipped (not defined in midiMap)\n");
+               return;
+       }
+
+       u::log::print("[MidiMapper::sendMidiLightning] learnt=0x%X, chan=%d, msg=0x%X, offset=%d\n",
+           learnt, m.channel, m.value, m.offset);
+
+       /* Isolate 'channel' from learnt message and offset it as requested by 'nn' in 
+       the midiMap configuration file. */
+
+       uint32_t out = ((learnt & 0x00FF0000) >> 16) << m.offset;
+
+       /* Merge the previously prepared channel into final message, and finally send 
+       it. */
+
+       out |= m.value | (m.channel << 24);
+       m_kernelMidi.send(out);
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+void MidiMapper<KernelMidiI>::parse(MidiMap::Message& message) const
+{
+       /* Remove '0x' part from the original string. */
+
+       std::string input = message.valueStr;
+
+       std::size_t f = input.find("0x"); // check if "0x" is there
+       if (f != std::string::npos)
+               input = message.valueStr.replace(f, 2, "");
+
+       /* Then transform string value into the actual uint32_t value, by parsing
+       each char (i.e. nibble) in the original string. Substitute 'n' with
+       zeros. */
+
+       std::string output;
+       for (unsigned i = 0, p = 24; i < input.length(); i++, p -= 4)
+       {
+               if (input[i] == 'n')
+               {
+                       output += '0';
+                       if (message.offset == -1) // do it once
+                               message.offset = p;
+               }
+               else
+                       output += input[i];
+       }
+
+       /* From string to uint32_t */
+
+       message.value = strtoul(output.c_str(), nullptr, 16);
+
+       u::log::print("[MidiMapper::parse] parsed chan=%d valueStr=%s value=%#x, offset=%d\n",
+           message.channel, message.valueStr, message.value, message.offset);
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+bool MidiMapper<KernelMidiI>::readCommand(const nl::json& j, MidiMap::Message& m, const std::string& key) const
+{
+       if (j.find(key) == j.end())
+               return false;
+
+       const nl::json& jc = j[key];
+
+       m.channel  = jc[MIDIMAP_KEY_CHANNEL];
+       m.valueStr = jc[MIDIMAP_KEY_MESSAGE];
+
+       return true;
+}
+/* -------------------------------------------------------------------------- */
+
+template <typename KernelMidiI>
+bool MidiMapper<KernelMidiI>::readInitCommands(MidiMap& midiMap, const nl::json& j)
+{
+       if (j.find(MIDIMAP_KEY_INIT_COMMANDS) == j.end())
+               return false;
+
+       for (const auto& jc : j[MIDIMAP_KEY_INIT_COMMANDS])
+       {
+               MidiMap::Message m;
+               m.channel  = jc[MIDIMAP_KEY_CHANNEL];
+               m.valueStr = jc[MIDIMAP_KEY_MESSAGE];
+               m.value    = strtoul(m.valueStr.c_str(), nullptr, 16);
+
+               midiMap.initCommands.push_back(m);
+       }
+
+       return true;
+}
+
+template class MidiMapper<KernelMidi>;
+#ifdef WITH_TESTS
+template class MidiMapper<KernelMidiMock>;
+#endif
+} // namespace giada::m
diff --git a/src/core/midiMapper.h b/src/core/midiMapper.h
new file mode 100644 (file)
index 0000000..f8f2c13
--- /dev/null
@@ -0,0 +1,148 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MIDIMAPPER_H
+#define G_MIDIMAPPER_H
+
+#include "deps/json/single_include/nlohmann/json.hpp"
+#include <string>
+#include <vector>
+
+namespace giada::m
+{
+class KernelMidi;
+#ifdef WITH_TESTS
+class KernelMidiMock;
+#endif
+} // namespace giada::m
+
+namespace giada::m
+{
+struct MidiMap
+{
+       struct Message
+       {
+               int         channel  = 0;
+               std::string valueStr = "";
+               int         offset   = -1;
+               uint32_t    value    = 0;
+       };
+
+       /* isValid
+       A valid MidiMap must have the brand and the device defined. */
+
+       bool isValid() const;
+
+       std::string          brand;
+       std::string          device;
+       std::vector<Message> initCommands;
+       Message              muteOn;
+       Message              muteOff;
+       Message              soloOn;
+       Message              soloOff;
+       Message              waiting;
+       Message              playing;
+       Message              stopping;
+       Message              stopped;
+       Message              playingInaudible;
+};
+
+template <typename KernelMidiI>
+class MidiMapper final
+{
+public:
+       MidiMapper(KernelMidiI&);
+
+       /* getMapFilesFound
+       Returns a reference to the list of midimaps found. */
+
+       const std::vector<std::string>& getMapFilesFound() const;
+
+       /* init
+       Parses the midimap folders and find the available midimaps. */
+
+       void init();
+
+       /* read
+       Reads a midimap from file 'file' and sets it as the current one. */
+
+       int read(const std::string& file);
+
+       /* sendInitMessages
+       Sends initialization messages from the midimap to the connected MIDI devices. */
+
+       void sendInitMessages(const MidiMap& midiMap);
+
+       /* sendMidiLightning
+       Sends a MIDI lightning message defined by 'msg'. */
+
+       void sendMidiLightning(uint32_t learnt, const MidiMap::Message& msg);
+
+       /* currentMap
+       The current MidiMap selected and loaded. It might be invalid if no midimaps
+       have been found. */
+
+       MidiMap currentMap;
+
+private:
+       KernelMidiI& m_kernelMidi;
+
+       /* isMessageDefined
+       Checks whether a specific message has been defined within a midimap file. */
+
+       bool isMessageDefined(const MidiMap::Message& m) const;
+
+       /* parse
+       Takes a string message with 'nn' in it and turns it into a real MIDI value.
+       TODO - don't edit message in place! */
+
+       void parse(MidiMap::Message& message) const;
+
+       /* TODO - don't edit midiMap in place! */
+       bool readInitCommands(MidiMap& midiMap, const nlohmann::json& j);
+
+       /* TODO - don't edit message in place! */
+       bool readCommand(const nlohmann::json& j, MidiMap::Message& m, const std::string& key) const;
+
+       /* m_mapsPath
+       Path to folder containing midimap files, different between OSes. */
+
+       std::string m_mapsPath;
+
+       /* m_mapFiles
+       The available .giadamap files. Each element of the vector represents 
+       a .giadamap file found in the midimap folder. */
+
+       std::vector<std::string> m_mapFiles;
+};
+
+extern template class MidiMapper<KernelMidi>;
+#ifdef WITH_TESTS
+extern template class MidiMapper<KernelMidiMock>;
+#endif
+} // namespace giada::m
+
+#endif
diff --git a/src/core/mixer.cpp b/src/core/mixer.cpp
new file mode 100644 (file)
index 0000000..bd49e43
--- /dev/null
@@ -0,0 +1,338 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/mixer.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "utils/log.h"
+#include "utils/math.h"
+
+namespace giada::m
+{
+namespace
+{
+/* CH_LEFT, CH_RIGHT
+Channels identifiers. */
+
+constexpr int CH_LEFT  = 0;
+constexpr int CH_RIGHT = 1;
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Mixer::Mixer(model::Model& m)
+: onSignalTresholdReached(nullptr)
+, onEndOfRecording(nullptr)
+, m_model(m)
+, m_signalCbFired(false)
+, m_endOfRecCbFired(false)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::reset(Frame maxFramesInLoop, Frame framesInBuffer)
+{
+       /* Allocate working buffers. rec buffer has variable size: it depends on how
+       many frames there are in the current loop. */
+
+       m_model.get().mixer.getRecBuffer().alloc(maxFramesInLoop, G_MAX_IO_CHANS);
+       m_model.get().mixer.getInBuffer().alloc(framesInBuffer, G_MAX_IO_CHANS);
+
+       u::log::print("[mixer::reset] buffers ready - maxFramesInLoop=%d, framesInBuffer=%d\n",
+           maxFramesInLoop, framesInBuffer);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Mixer::isActive() const { return m_model.get().mixer.a_isActive(); }
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::enable()
+{
+       m_model.get().mixer.a_setActive(true);
+       u::log::print("[mixer::enable] enabled\n");
+}
+
+void Mixer::disable()
+{
+       m_model.get().mixer.a_setActive(false);
+       while (m_model.isLocked())
+               ;
+       u::log::print("[mixer::disable] disabled\n");
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::allocRecBuffer(Frame frames)
+{
+       m_model.get().mixer.getRecBuffer().alloc(frames, G_MAX_IO_CHANS);
+}
+
+void Mixer::clearRecBuffer()
+{
+       m_model.get().mixer.getRecBuffer().clear();
+}
+
+const mcl::AudioBuffer& Mixer::getRecBuffer()
+{
+       return m_model.get().mixer.getRecBuffer();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::advanceChannels(const Sequencer::EventBuffer& events,
+    const model::Layout& rtLayout, Range<Frame> block, Frame quantizerStep)
+{
+       for (const Channel& c : rtLayout.channels)
+               if (!c.isInternal())
+                       c.advance(events, block, quantizerStep);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::render(mcl::AudioBuffer& out, const mcl::AudioBuffer& in, const model::Layout& layout_RT) const
+{
+       const model::Mixer&     mixer     = layout_RT.mixer;
+       const model::Sequencer& sequencer = layout_RT.sequencer;
+       const model::Recorder&  recorder  = layout_RT.recorder;
+
+       const Channel& masterOutCh = layout_RT.getChannel(Mixer::MASTER_OUT_CHANNEL_ID);
+       const Channel& masterInCh  = layout_RT.getChannel(Mixer::MASTER_IN_CHANNEL_ID);
+       const Channel& previewCh   = layout_RT.getChannel(Mixer::PREVIEW_CHANNEL_ID);
+
+       const bool  hasInput        = in.isAllocd();
+       const bool  inToOut         = mixer.inToOut;
+       const bool  isSeqActive     = sequencer.isActive();
+       const bool  shouldLineInRec = isSeqActive && recorder.a_isRecordingInput() && hasInput;
+       const float recTriggerLevel = mixer.recTriggerLevel;
+       const Frame maxFramesToRec  = mixer.maxFramesToRec;
+       const bool  allowsOverdub   = mixer.allowsOverdub;
+       const bool  limitOutput     = mixer.limitOutput;
+
+       mixer.getInBuffer().clear();
+
+       /* Reset peak computation. */
+
+       mixer.a_setPeakOut({0.0f, 0.0f});
+       mixer.a_setPeakIn({0.0f, 0.0f});
+
+       if (hasInput)
+       {
+               processLineIn(mixer, in, masterInCh.volume, recTriggerLevel, isSeqActive);
+               renderMasterIn(masterInCh, mixer.getInBuffer());
+       }
+
+       if (shouldLineInRec)
+       {
+               const Frame newTrackerPos = lineInRec(in, mixer.getRecBuffer(),
+                   mixer.a_getInputTracker(), maxFramesToRec, masterInCh.volume,
+                   allowsOverdub);
+               mixer.a_setInputTracker(newTrackerPos);
+       }
+
+       /* Channel processing. Don't do it if layout is locked: another thread is 
+       changing data (e.g. Plugins or Waves). */
+
+       if (!layout_RT.locked)
+               renderChannels(layout_RT.channels, out, mixer.getInBuffer());
+
+       /* Render remaining internal channels. */
+
+       renderMasterOut(masterOutCh, out);
+       renderPreview(previewCh, out);
+
+       /* Post processing. */
+
+       finalizeOutput(mixer, out, inToOut, limitOutput, masterOutCh.volume);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::startInputRec(Frame from)
+{
+       m_model.get().mixer.a_setInputTracker(from);
+}
+
+Frame Mixer::stopInputRec()
+{
+       const Frame ret = m_model.get().mixer.a_getInputTracker();
+       m_model.get().mixer.a_setInputTracker(0);
+       m_signalCbFired   = false;
+       m_endOfRecCbFired = false;
+       return ret;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Mixer::isChannelAudible(const Channel& c) const
+{
+       if (c.isInternal())
+               return true;
+       if (c.isMuted())
+               return false;
+       const bool hasSolos = m_model.get().mixer.hasSolos;
+       return !hasSolos || (hasSolos && c.isSoloed());
+}
+
+/* -------------------------------------------------------------------------- */
+
+Peak Mixer::getPeakOut() const { return m_model.get().mixer.a_getPeakOut(); }
+Peak Mixer::getPeakIn() const { return m_model.get().mixer.a_getPeakIn(); }
+
+/* -------------------------------------------------------------------------- */
+
+Mixer::RecordInfo Mixer::getRecordInfo() const
+{
+       return {
+           m_model.get().mixer.a_getInputTracker(),
+           m_model.get().mixer.getRecBuffer().countFrames()};
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Mixer::thresholdReached(Peak p, float threshold) const
+{
+       return u::math::linearToDB(p.left) > threshold ||
+              u::math::linearToDB(p.right) > threshold;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Peak Mixer::makePeak(const mcl::AudioBuffer& b) const
+{
+       if (!b.isAllocd())
+               return {0.0f, 0.0f};
+       return {b.getPeak(CH_LEFT), b.getPeak(b.countChannels() == 1 ? CH_LEFT : CH_RIGHT)};
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame Mixer::lineInRec(const mcl::AudioBuffer& inBuf, mcl::AudioBuffer& recBuf, Frame inputTracker,
+    Frame maxFrames, float inVol, bool allowsOverdub) const
+{
+       assert(maxFrames > 0 && maxFrames <= recBuf.countFrames());
+       assert(onEndOfRecording != nullptr);
+
+       if (inputTracker >= maxFrames && !allowsOverdub && !m_endOfRecCbFired)
+       {
+               onEndOfRecording();
+               m_endOfRecCbFired = true;
+               return 0;
+       }
+
+       const Frame framesToCopy = -1; // copy everything
+       const Frame srcOffset    = 0;
+       const Frame destOffset   = inputTracker % maxFrames; // loop over at maxFrames
+
+       recBuf.sum(inBuf, framesToCopy, srcOffset, destOffset, inVol);
+
+       return inputTracker + inBuf.countFrames();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::processLineIn(const model::Mixer& mixer, const mcl::AudioBuffer& inBuf,
+    float inVol, float recTriggerLevel, bool isSeqActive) const
+{
+       const Peak peak = makePeak(inBuf);
+
+       if (thresholdReached(peak, recTriggerLevel) && !m_signalCbFired && isSeqActive)
+       {
+               m_signalCbFired = true;
+               onSignalTresholdReached();
+               G_DEBUG("Signal > threshold!");
+       }
+
+       mixer.a_setPeakIn(peak);
+
+       /* Prepare the working buffer for input stream, which will be processed 
+       later on by the Master Input Channel with plug-ins. */
+
+       assert(inBuf.countChannels() <= mixer.getInBuffer().countChannels());
+
+       mixer.getInBuffer().set(inBuf, inVol);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::renderChannels(const std::vector<Channel>& channels, mcl::AudioBuffer& out, mcl::AudioBuffer& in) const
+{
+       for (const Channel& c : channels)
+               if (!c.isInternal())
+                       c.render(&out, &in, isChannelAudible(c));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::renderMasterIn(const Channel& ch, mcl::AudioBuffer& in) const
+{
+#ifdef WITH_VST
+       ch.render(nullptr, &in, true);
+#else
+       (void)ch;
+       (void)in;
+#endif
+}
+
+void Mixer::renderMasterOut(const Channel& ch, mcl::AudioBuffer& out) const
+{
+       ch.render(&out, nullptr, true);
+}
+
+void Mixer::renderPreview(const Channel& ch, mcl::AudioBuffer& out) const
+{
+       ch.render(&out, nullptr, true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::limit(mcl::AudioBuffer& outBuf) const
+{
+       for (int i = 0; i < outBuf.countFrames(); i++)
+               for (int j = 0; j < outBuf.countChannels(); j++)
+                       outBuf[i][j] = std::max(-1.0f, std::min(outBuf[i][j], 1.0f));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::finalizeOutput(const model::Mixer& mixer, mcl::AudioBuffer& buf,
+    bool inToOut, bool shouldLimit, float vol) const
+{
+       if (inToOut)
+               buf.sum(mixer.getInBuffer(), vol);
+       else
+               buf.applyGain(vol);
+
+       if (shouldLimit)
+               limit(buf);
+
+       mixer.a_setPeakOut({buf.getPeak(CH_LEFT), buf.getPeak(CH_RIGHT)});
+}
+} // namespace giada::m
diff --git a/src/core/mixer.h b/src/core/mixer.h
new file mode 100644 (file)
index 0000000..d10c933
--- /dev/null
@@ -0,0 +1,209 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MIXER_H
+#define G_MIXER_H
+
+#include "core/midiEvent.h"
+#include "core/queue.h"
+#include "core/ringBuffer.h"
+#include "core/sequencer.h"
+#include "core/types.h"
+#include "core/weakAtomic.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include "src/core/actions/actions.h"
+#include <functional>
+
+namespace mcl
+{
+class AudioBuffer;
+}
+
+namespace giada::m::model
+{
+class Mixer;
+struct Layout;
+} // namespace giada::m::model
+
+namespace giada::m
+{
+struct Action;
+class Channel;
+class MixerHandler;
+class Mixer
+{
+public:
+       friend MixerHandler;
+
+       static constexpr int MASTER_OUT_CHANNEL_ID = 1;
+       static constexpr int MASTER_IN_CHANNEL_ID  = 2;
+       static constexpr int PREVIEW_CHANNEL_ID    = 3;
+
+       /* RecordInfo
+       Information regarding the input recording progress. */
+
+       struct RecordInfo
+       {
+               Frame position;
+               Frame maxLength;
+       };
+
+       Mixer(model::Model&);
+
+       /* isActive
+       Mixer might be inactive (not initialized or suspended). */
+
+       bool isActive() const;
+
+       /* isChannelAudible
+       True if the channel 'c' is currently audible: not muted or not included in a 
+       solo session. */
+
+       bool isChannelAudible(const Channel& c) const;
+
+       Peak getPeakOut() const;
+       Peak getPeakIn() const;
+
+       /* getRecordInfo
+       Returns information on the ongoing input recording. */
+
+       RecordInfo getRecordInfo() const;
+
+       /* render
+       Core rendering function. */
+
+       void render(mcl::AudioBuffer& out, const mcl::AudioBuffer& in, const model::Layout&) const;
+
+       /* reset
+       Brings everything back to the initial state. */
+
+       void reset(Frame framesInLoop, Frame framesInBuffer);
+
+       /* enable, disable
+       Toggles master callback processing. Useful to suspend the rendering. */
+
+       void enable();
+       void disable();
+
+       /* allocRecBuffer
+       Allocates new memory for the virtual input channel. */
+
+       void allocRecBuffer(Frame frames);
+
+       /* clearRecBuffer
+       Clears internal virtual channel. */
+
+       void clearRecBuffer();
+
+       /* getRecBuffer
+       Returns a read-only reference to the internal virtual channel. Use this to
+       merge data into channel after an input recording session. */
+
+       const mcl::AudioBuffer& getRecBuffer();
+
+       /* advanceChannels
+       Processes Channels' static events (e.g. pre-recorded actions or sequencer 
+       events) in the current audio block. Called by the main audio thread when the 
+       sequencer is running. */
+
+       void advanceChannels(const Sequencer::EventBuffer&, const model::Layout&,
+           Range<Frame>, Frame quantizerStep);
+
+       /* onSignalTresholdReached
+       Callback fired when audio has reached a certain threshold (record-on-signal 
+       mode). */
+
+       std::function<void()> onSignalTresholdReached;
+
+       /* onEndOfRecording
+       Callback fired when the audio recording session has ended. */
+
+       std::function<void()> onEndOfRecording;
+
+private:
+       /* thresholdReached
+       Returns true if left or right channel's peak has reached a certain 
+       threshold. */
+
+       bool thresholdReached(Peak p, float threshold) const;
+
+       /* makePeak
+       Returns a Peak object given an audio buffer, taking number of channels into
+       account. */
+
+       Peak makePeak(const mcl::AudioBuffer& b) const;
+
+       /* lineInRec
+       Records from line in. 'maxFrames' determines how many frames to record 
+       before the internal tracker loops over. The value changes whether you are 
+       recording in RIGID or FREE mode. Returns the number of recorded frames. */
+
+       Frame lineInRec(const mcl::AudioBuffer& inBuf, mcl::AudioBuffer& recBuf,
+           Frame inputTracker, Frame maxFrames, float inVol, bool allowsOverdub) const;
+
+       /* processLineIn
+       Computes line in peaks and prepares the internal working buffer for input
+       recording. */
+
+       void processLineIn(const model::Mixer& mixer, const mcl::AudioBuffer& inBuf,
+           float inVol, float recTriggerLevel, bool isSeqActive) const;
+
+       void renderChannels(const std::vector<Channel>& channels, mcl::AudioBuffer& out, mcl::AudioBuffer& in) const;
+       void renderMasterIn(const Channel&, mcl::AudioBuffer& in) const;
+       void renderMasterOut(const Channel&, mcl::AudioBuffer& out) const;
+       void renderPreview(const Channel&, mcl::AudioBuffer& out) const;
+
+       /* limit
+       Applies a very dumb hard limiter. */
+
+       void limit(mcl::AudioBuffer& outBuf) const;
+
+       /* finalizeOutput
+       Last touches after the output has been rendered: apply inToOut if any, apply
+       output volume, compute peak. */
+
+       void finalizeOutput(const model::Mixer&, mcl::AudioBuffer&, bool inToOut,
+           bool limit, float vol) const;
+
+       /* startInputRec, stopInputRec
+       Starts/stops input recording on frame 'from'. The latter returns the number 
+       of recorded frames. */
+
+       void  startInputRec(Frame from);
+       Frame stopInputRec();
+
+       model::Model& m_model;
+
+       /* m_signalCbFired, m_endOfRecCbFired
+       Boolean guards to determine whether the callbacks have been fired or not, 
+       to avoid retriggering. Mutable: strictly for internal use only. */
+
+       mutable bool m_signalCbFired;
+       mutable bool m_endOfRecCbFired;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/mixerHandler.cpp b/src/core/mixerHandler.cpp
new file mode 100644 (file)
index 0000000..af88cf2
--- /dev/null
@@ -0,0 +1,427 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/mixerHandler.h"
+#include "core/channels/channelManager.h"
+#include "core/const.h"
+#include "core/mixer.h"
+#include "core/model/model.h"
+#include "core/plugins/pluginManager.h"
+#include "core/recorder.h"
+#include "glue/channel.h"
+#include "glue/main.h"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include "utils/vector.h"
+#include <algorithm>
+#include <cassert>
+#include <memory>
+#include <vector>
+
+namespace giada::m
+{
+MixerHandler::MixerHandler(model::Model& model, Mixer& mixer)
+: onChannelsAltered(nullptr)
+, onChannelRecorded(nullptr)
+, m_model(model)
+, m_mixer(mixer)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::reset(Frame framesInLoop, Frame framesInBuffer, ChannelManager& channelManager)
+{
+       m_mixer.reset(framesInLoop, framesInBuffer);
+
+       m_model.get().channels.clear();
+
+       m_model.get().channels.push_back(channelManager.create(
+           Mixer::MASTER_OUT_CHANNEL_ID, ChannelType::MASTER, /*columnId=*/0, framesInBuffer));
+       m_model.get().channels.push_back(channelManager.create(
+           Mixer::MASTER_IN_CHANNEL_ID, ChannelType::MASTER, /*columnId=*/0, framesInBuffer));
+       m_model.get().channels.push_back(channelManager.create(
+           Mixer::PREVIEW_CHANNEL_ID, ChannelType::PREVIEW, /*columnId=*/0, framesInBuffer));
+
+       m_model.swap(model::SwapType::NONE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel& MixerHandler::addChannel(ChannelType type, ID columnId, int bufferSize,
+    ChannelManager& channelManager)
+{
+       m_model.get().channels.push_back(channelManager.create(/*id=*/0, type, columnId, bufferSize));
+       m_model.swap(model::SwapType::HARD);
+
+       return m_model.get().channels.back();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::loadChannel(ID channelId, std::unique_ptr<Wave> w)
+{
+       assert(onChannelsAltered != nullptr);
+
+       m_model.addShared(std::move(w));
+
+       Channel& channel = m_model.get().getChannel(channelId);
+       Wave&    wave    = m_model.backShared<Wave>();
+       Wave*    old     = channel.samplePlayer->getWave();
+
+       loadChannel(channel, &wave);
+       m_model.swap(model::SwapType::HARD);
+
+       /* Remove old wave, if any. It is safe to do it now: the audio thread is
+       already processing the new layout. */
+
+       if (old != nullptr)
+               m_model.removeShared<Wave>(*old);
+
+       onChannelsAltered();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::addAndLoadChannel(ID columnId, std::unique_ptr<Wave> w, int bufferSize,
+    ChannelManager& channelManager)
+{
+       assert(onChannelsAltered != nullptr);
+
+       m_model.addShared(std::move(w));
+
+       Wave&    wave    = m_model.backShared<Wave>();
+       Channel& channel = addChannel(ChannelType::SAMPLE, columnId, bufferSize, channelManager);
+
+       loadChannel(channel, &wave);
+       m_model.swap(model::SwapType::HARD);
+
+       onChannelsAltered();
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+void MixerHandler::cloneChannel(ID channelId, int sampleRate, int bufferSize,
+    ChannelManager& channelManager, WaveManager& waveManager, const Sequencer& sequencer,
+    PluginManager& pluginManager)
+#else
+void MixerHandler::cloneChannel(ID channelId, int bufferSize, ChannelManager& channelManager,
+    WaveManager& waveManager)
+#endif
+{
+       const Channel& oldChannel = m_model.get().getChannel(channelId);
+       Channel        newChannel = channelManager.create(oldChannel, bufferSize);
+
+       /* Clone waves and plugins first in their own lists. */
+
+       if (oldChannel.samplePlayer && oldChannel.samplePlayer->hasWave())
+       {
+               const Wave& oldWave = *oldChannel.samplePlayer->getWave();
+               m_model.addShared(waveManager.createFromWave(oldWave, 0, oldWave.getBuffer().countFrames()));
+               loadChannel(newChannel, &m_model.backShared<Wave>());
+       }
+
+#ifdef WITH_VST
+
+       /* Overwrite existing plug-ins in new channel with a new vector of plug-ins,
+       as currently new channel has cloned plug-ins from the old one (with same ID). */
+
+       std::vector<Plugin*> newPlugins;
+       for (const Plugin* plugin : oldChannel.plugins)
+       {
+               m_model.addShared(pluginManager.makePlugin(*plugin, sampleRate, bufferSize, sequencer));
+               newPlugins.push_back(&m_model.backShared<Plugin>());
+       }
+       newChannel.plugins = newPlugins;
+
+#endif
+
+       /* Then push the new channel in the channels vector. */
+
+       m_model.get().channels.push_back(newChannel);
+       m_model.swap(model::SwapType::HARD);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::freeChannel(ID channelId)
+{
+       assert(onChannelsAltered != nullptr);
+
+       Channel& ch = m_model.get().getChannel(channelId);
+
+       assert(ch.samplePlayer);
+
+       const Wave* wave = ch.samplePlayer->getWave();
+
+       loadChannel(ch, nullptr);
+       m_model.swap(model::SwapType::HARD);
+
+       if (wave != nullptr)
+               m_model.removeShared<Wave>(*wave);
+
+       onChannelsAltered();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::freeAllChannels()
+{
+       assert(onChannelsAltered != nullptr);
+
+       for (Channel& ch : m_model.get().channels)
+               if (ch.samplePlayer)
+                       loadChannel(ch, nullptr);
+
+       m_model.swap(model::SwapType::HARD);
+       m_model.clearShared<model::WavePtrs>();
+
+       onChannelsAltered();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::deleteChannel(ID channelId)
+{
+       assert(onChannelsAltered != nullptr);
+
+       const Channel& ch   = m_model.get().getChannel(channelId);
+       const Wave*    wave = ch.samplePlayer ? ch.samplePlayer->getWave() : nullptr;
+#ifdef WITH_VST
+       const std::vector<Plugin*> plugins = ch.plugins;
+#endif
+
+       u::vector::removeIf(m_model.get().channels, [channelId](const Channel& c) {
+               return c.id == channelId;
+       });
+       m_model.swap(model::SwapType::HARD);
+
+       if (wave != nullptr)
+               m_model.removeShared<Wave>(*wave);
+
+       updateSoloCount();
+       onChannelsAltered();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::renameChannel(ID channelId, const std::string& name)
+{
+       m_model.get().getChannel(channelId).name = name;
+       m_model.swap(model::SwapType::HARD);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::updateSoloCount()
+{
+       bool hasSolos = forAnyChannel([](const Channel& ch) {
+               return !ch.isInternal() && ch.isSoloed();
+       });
+
+       m_model.get().mixer.hasSolos = hasSolos;
+       m_model.swap(model::SwapType::NONE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::setInToOut(bool v)
+{
+       m_model.get().mixer.inToOut = v;
+       m_model.swap(model::SwapType::NONE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+float MixerHandler::getInVol() const
+{
+       return m_model.get().getChannel(Mixer::MASTER_IN_CHANNEL_ID).volume;
+}
+
+float MixerHandler::getOutVol() const
+{
+       return m_model.get().getChannel(Mixer::MASTER_OUT_CHANNEL_ID).volume;
+}
+
+bool MixerHandler::getInToOut() const
+{
+       return m_model.get().mixer.inToOut;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::startInputRec(Frame currentFrame)
+{
+       m_mixer.startInputRec(currentFrame);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame MixerHandler::stopInputRec()
+{
+       return m_mixer.stopInputRec();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::finalizeInputRec(Frame recordedFrames, Frame currentFrame)
+{
+       for (Channel* ch : getRecordableChannels())
+               recordChannel(*ch, recordedFrames, currentFrame);
+       for (Channel* ch : getOverdubbableChannels())
+               overdubChannel(*ch, currentFrame);
+
+       m_mixer.clearRecBuffer();
+
+       onChannelsAltered();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool MixerHandler::hasInputRecordableChannels() const
+{
+       return forAnyChannel([](const Channel& ch) { return ch.canInputRec(); });
+}
+
+bool MixerHandler::hasActionRecordableChannels() const
+{
+       return forAnyChannel([](const Channel& ch) { return ch.canActionRec(); });
+}
+
+bool MixerHandler::hasLogicalSamples() const
+{
+       return forAnyChannel([](const Channel& ch) { return ch.samplePlayer && ch.samplePlayer->hasLogicalWave(); });
+}
+
+bool MixerHandler::hasEditedSamples() const
+{
+       return forAnyChannel([](const Channel& ch) {
+               return ch.samplePlayer && ch.samplePlayer->hasEditedWave();
+       });
+}
+
+bool MixerHandler::hasActions() const
+{
+       return forAnyChannel([](const Channel& ch) { return ch.hasActions; });
+}
+
+bool MixerHandler::hasAudioData() const
+{
+       return forAnyChannel([](const Channel& ch) {
+               return ch.samplePlayer && ch.samplePlayer->hasWave();
+       });
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool MixerHandler::forAnyChannel(std::function<bool(const Channel&)> f) const
+{
+       return std::any_of(m_model.get().channels.begin(), m_model.get().channels.end(), f);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::loadChannel(Channel& ch, Wave* w) const
+{
+       ch.samplePlayer->loadWave(*ch.shared, w);
+       ch.name = w != nullptr ? w->getBasename(/*ext=*/false) : "";
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<Channel*> MixerHandler::getChannelsIf(std::function<bool(const Channel&)> f)
+{
+       std::vector<Channel*> out;
+       for (Channel& ch : m_model.get().channels)
+               if (f(ch))
+                       out.push_back(&ch);
+       return out;
+}
+
+std::vector<Channel*> MixerHandler::getRecordableChannels()
+{
+       return getChannelsIf([](const Channel& c) { return c.canInputRec() && !c.hasWave(); });
+}
+
+std::vector<Channel*> MixerHandler::getOverdubbableChannels()
+{
+       return getChannelsIf([](const Channel& c) { return c.canInputRec() && c.hasWave(); });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::setupChannelPostRecording(Channel& ch, Frame currentFrame)
+{
+       /* Start sample channels in loop mode right away. */
+       if (ch.samplePlayer->isAnyLoopMode())
+               ch.samplePlayer->kickIn(*ch.shared, currentFrame);
+       /* Disable 'arm' button if overdub protection is on. */
+       if (ch.audioReceiver->overdubProtection == true)
+               ch.armed = false;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::recordChannel(Channel& ch, Frame recordedFrames, Frame currentFrame)
+{
+       assert(onChannelRecorded != nullptr);
+
+       std::unique_ptr<Wave> wave = onChannelRecorded(recordedFrames);
+
+       G_DEBUG("Created new Wave, size=" << wave->getBuffer().countFrames());
+
+       /* Copy up to wave.getSize() from the mixer's input buffer into wave's. */
+
+       wave->getBuffer().set(m_mixer.getRecBuffer(), wave->getBuffer().countFrames());
+
+       /* Update channel with the new Wave. */
+
+       m_model.addShared(std::move(wave));
+       loadChannel(ch, &m_model.backShared<Wave>());
+       setupChannelPostRecording(ch, currentFrame);
+
+       m_model.swap(model::SwapType::HARD);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void MixerHandler::overdubChannel(Channel& ch, Frame currentFrame)
+{
+       Wave* wave = ch.samplePlayer->getWave();
+
+       /* Need model::DataLock here, as data might be being read by the audio
+       thread at the same time. */
+
+       model::DataLock lock = m_model.lockData();
+
+       wave->getBuffer().sum(m_mixer.getRecBuffer(), /*gain=*/1.0f);
+       wave->setLogical(true);
+
+       setupChannelPostRecording(ch, currentFrame);
+}
+} // namespace giada::m
diff --git a/src/core/mixerHandler.h b/src/core/mixerHandler.h
new file mode 100644 (file)
index 0000000..65364c9
--- /dev/null
@@ -0,0 +1,190 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MIXER_HANDLER_H
+#define G_MIXER_HANDLER_H
+
+#include "core/plugins/pluginManager.h"
+#include "core/waveManager.h"
+#include "types.h"
+#include <functional>
+#include <memory>
+#include <string>
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::m
+{
+class Channel;
+class Wave;
+class Mixer;
+class Plugin;
+class ChannelManager;
+class Sequencer;
+class MixerHandler final
+{
+public:
+       MixerHandler(model::Model&, Mixer&);
+
+       /* hasLogicalSamples
+    True if 1 or more samples are logical (memory only, such as takes). */
+
+       bool hasLogicalSamples() const;
+
+       /* hasEditedSamples
+    True if 1 or more samples have been edited via Sample Editor. */
+
+       bool hasEditedSamples() const;
+
+       /* has(Input|Action)RecordableChannels
+    Tells whether Mixer has one or more input or action recordable channels. */
+
+       bool hasInputRecordableChannels() const;
+       bool hasActionRecordableChannels() const;
+
+       /* hasActions
+    True if at least one Channel has actions recorded in it. */
+
+       bool hasActions() const;
+
+       /* hasAudioData
+    True if at least one Sample Channel has some audio recorded in it. */
+
+       bool hasAudioData() const;
+
+       float getInVol() const;
+       float getOutVol() const;
+       bool  getInToOut() const;
+
+       /* reset
+       Brings everything back to the initial state. */
+
+       void reset(Frame framesInLoop, Frame framesInBuffer, ChannelManager&);
+
+       /* addChannel
+    Adds a new channel of type 'type' into the channels stack. Returns the new
+    channel ID. */
+
+       Channel& addChannel(ChannelType type, ID columnId, int bufferSize, ChannelManager&);
+
+       /* loadChannel
+    Loads a new Wave inside a Sample Channel. */
+
+       void loadChannel(ID channelId, std::unique_ptr<Wave> w);
+
+       /* addAndLoadChannel
+    Creates a new channels, fills it with a Wave and then add it to the stack. */
+
+       void addAndLoadChannel(ID columnId, std::unique_ptr<Wave> w, int bufferSize,
+           ChannelManager&);
+
+       /* freeChannel
+    Unloads existing Wave from a Sample Channel. */
+
+       void freeChannel(ID channelId);
+
+       /* deleteChannel
+    Completely removes a channel from the stack. */
+
+       void deleteChannel(ID channelId);
+
+#ifdef WITH_VST
+       void cloneChannel(ID channelId, int sampleRate, int bufferSize, ChannelManager&,
+           WaveManager&, const Sequencer&, PluginManager&);
+#else
+       void cloneChannel(ID channelId, int bufferSize, ChannelManager&, WaveManager&);
+#endif
+       void renameChannel(ID channelId, const std::string& name);
+       void freeAllChannels();
+
+       void setInToOut(bool v);
+
+       /* updateSoloCount
+    Updates the number of solo-ed channels in mixer. */
+
+       void updateSoloCount();
+
+       /* startInputRec
+       Initializes Mixer for input recording. */
+
+       void startInputRec(Frame currentFrame);
+
+       /* stopInputRec
+       Terminates input recording in Mixer. Returns the number of recorded frames. 
+       Call finalizeInputRec() if you really want to finish the input recording 
+       operation. */
+
+       Frame stopInputRec();
+
+       /* finalizeInputRec
+    Fills armed Sample Channels with audio data coming from an input recording
+    session. */
+
+       void finalizeInputRec(Frame recordedFrames, Frame currentFrame);
+
+       /* onChannelsAltered
+       Fired when something is done on channels (added, removed, loaded, ...). */
+
+       std::function<void()> onChannelsAltered;
+
+       /* onChannelRecorded
+       Fired during the input recording finalization, when a new empty Wave must
+       be added to each armed channel in order to store recorded audio coming from
+       Mixer. */
+
+       std::function<std::unique_ptr<Wave>(Frame)> onChannelRecorded;
+
+private:
+       bool forAnyChannel(std::function<bool(const Channel&)> f) const;
+
+       void loadChannel(Channel&, Wave*) const;
+
+       std::vector<Channel*> getChannelsIf(std::function<bool(const Channel&)> f);
+       std::vector<Channel*> getRecordableChannels();
+       std::vector<Channel*> getOverdubbableChannels();
+
+       void setupChannelPostRecording(Channel& ch, Frame currentFrame);
+
+       /* recordChannel
+       Records the current Mixer audio input data into an empty channel. */
+
+       void recordChannel(Channel& ch, Frame recordedFrames, Frame currentFrame);
+
+       /* overdubChannel
+       Records the current Mixer audio input data into a channel with an existing
+       Wave, overdub mode. */
+
+       void overdubChannel(Channel& ch, Frame currentFrame);
+
+       model::Model& m_model;
+       Mixer&        m_mixer;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/model/mixer.cpp b/src/core/model/mixer.cpp
new file mode 100644 (file)
index 0000000..7ed5dd7
--- /dev/null
@@ -0,0 +1,102 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/model/mixer.h"
+
+namespace giada::m::model
+{
+Mixer::Shared& Mixer::Shared::operator=(const Mixer::Shared& o)
+{
+       if (this == &o)
+               return *this;
+       active.store(o.active.load());
+       peakOutL.store(0.0f);
+       peakOutR.store(0.0f);
+       peakInL.store(0.0f);
+       peakInR.store(0.0f);
+       inputTracker.store(0);
+       return *this;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Mixer::a_isActive() const
+{
+       return shared->active.load() == true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame Mixer::a_getInputTracker() const
+{
+       return shared->inputTracker.load();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::a_setActive(bool isActive) const
+{
+       shared->active.store(isActive);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::a_setInputTracker(Frame f) const
+{
+       shared->inputTracker.store(f);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Peak Mixer::a_getPeakOut() const
+{
+       return {shared->peakOutL.load(), shared->peakOutR.load()};
+}
+
+Peak Mixer::a_getPeakIn() const
+{
+       return {shared->peakInL.load(), shared->peakInR.load()};
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Mixer::a_setPeakOut(Peak p) const
+{
+       shared->peakOutL.store(p.left);
+       shared->peakOutR.store(p.right);
+}
+
+void Mixer::a_setPeakIn(Peak p) const
+{
+       shared->peakInL.store(p.left);
+       shared->peakInR.store(p.right);
+}
+
+/* -------------------------------------------------------------------------- */
+
+mcl::AudioBuffer& Mixer::getRecBuffer() const { return shared->recBuffer; }
+mcl::AudioBuffer& Mixer::getInBuffer() const { return shared->inBuffer; }
+} // namespace giada::m::model
diff --git a/src/core/model/mixer.h b/src/core/model/mixer.h
new file mode 100644 (file)
index 0000000..a5a7499
--- /dev/null
@@ -0,0 +1,90 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MODEL_MIXER_H
+#define G_MODEL_MIXER_H
+
+#include "core/const.h"
+#include "core/types.h"
+#include "core/weakAtomic.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include <atomic>
+
+namespace giada::m::model
+{
+class Mixer
+{
+       friend class Model;
+
+public:
+       bool  a_isActive() const;
+       Frame a_getInputTracker() const;
+       Peak  a_getPeakOut() const;
+       Peak  a_getPeakIn() const;
+
+       void a_setActive(bool) const;
+       void a_setInputTracker(Frame) const;
+       void a_setPeakOut(Peak) const;
+       void a_setPeakIn(Peak) const;
+
+       mcl::AudioBuffer& getRecBuffer() const;
+       mcl::AudioBuffer& getInBuffer() const;
+
+       bool  hasSolos        = false;
+       bool  inToOut         = false;
+       bool  limitOutput     = false;
+       bool  allowsOverdub   = false;
+       Frame maxFramesToRec  = 0;
+       float recTriggerLevel = 0.0f;
+
+private:
+       struct Shared
+       {
+               Shared& operator=(const Shared&);
+
+               std::atomic<bool> active       = false;
+               WeakAtomic<float> peakOutL     = 0.0f;
+               WeakAtomic<float> peakOutR     = 0.0f;
+               WeakAtomic<float> peakInL      = 0.0f;
+               WeakAtomic<float> peakInR      = 0.0f;
+               WeakAtomic<Frame> inputTracker = 0;
+
+               /* recBuffer
+               Working buffer for audio recording. */
+
+               mcl::AudioBuffer recBuffer;
+
+               /* inBuffer
+               Working buffer for input channel. Used for the in->out bridge. */
+
+               mcl::AudioBuffer inBuffer;
+       };
+
+       Shared* shared = nullptr;
+};
+} // namespace giada::m::model
+
+#endif
diff --git a/src/core/model/model.cpp b/src/core/model/model.cpp
new file mode 100644 (file)
index 0000000..f7b7daa
--- /dev/null
@@ -0,0 +1,340 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/model/model.h"
+#include <cassert>
+#include <memory>
+#ifdef G_DEBUG_MODE
+#include "core/channels/channelManager.h"
+#endif
+
+using namespace mcl;
+
+namespace giada::m::model
+{
+namespace
+{
+template <typename T>
+auto getIter_(const std::vector<std::unique_ptr<T>>& source, ID id)
+{
+       return u::vector::findIf(source, [id](const std::unique_ptr<T>& p) { return p->id == id; });
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename S>
+auto* get_(S& source, ID id)
+{
+       auto it = getIter_(source, id);
+       return it == source.end() ? nullptr : it->get();
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename D, typename T>
+void remove_(D& dest, T& ref)
+{
+       u::vector::removeIf(dest, [&ref](const auto& other) { return other.get() == &ref; });
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+DataLock::DataLock(Model& m, SwapType t)
+: m_model(m)
+, m_swapType(t)
+{
+       m_model.get().locked = true;
+       m_model.swap(SwapType::NONE);
+}
+
+DataLock::~DataLock()
+{
+       m_model.get().locked = false;
+       m_model.swap(m_swapType);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Channel& Layout::getChannel(ID id)
+{
+       return const_cast<Channel&>(const_cast<const Layout*>(this)->getChannel(id));
+}
+
+const Channel& Layout::getChannel(ID id) const
+{
+       auto it = std::find_if(channels.begin(), channels.end(), [id](const Channel& c) {
+               return c.id == id;
+       });
+       assert(it != channels.end());
+       return *it;
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Model::Model()
+: onSwap(nullptr)
+{
+       reset();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Model::reset()
+{
+       get()    = {};
+       m_shared = {};
+
+       get().sequencer.shared = &m_shared.sequencerShared;
+       get().mixer.shared     = &m_shared.mixerShared;
+       get().recorder.shared  = &m_shared.recorderShared;
+
+       swap(SwapType::NONE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Layout&       Model::get() { return m_layout.get(); }
+const Layout& Model::get() const { return m_layout.get(); }
+
+LayoutLock Model::get_RT()
+{
+       return LayoutLock(m_layout);
+}
+
+void Model::swap(SwapType t)
+{
+       m_layout.swap();
+       if (onSwap)
+               onSwap(t);
+}
+
+/* -------------------------------------------------------------------------- */
+
+DataLock Model::lockData(SwapType t)
+{
+       return DataLock(*this, t);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Model::isLocked() const
+{
+       return m_layout.isLocked();
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T>
+T& Model::getAllShared()
+{
+#ifdef WITH_VST
+       if constexpr (std::is_same_v<T, PluginPtrs>)
+               return m_shared.plugins;
+#endif
+       if constexpr (std::is_same_v<T, WavePtrs>)
+               return m_shared.waves;
+       if constexpr (std::is_same_v<T, Actions::Map>)
+               return m_shared.actions;
+       if constexpr (std::is_same_v<T, ChannelSharedPtrs>)
+               return m_shared.channelsShared;
+
+       assert(false);
+}
+
+#ifdef WITH_VST
+template PluginPtrs& Model::getAllShared<PluginPtrs>();
+#endif
+template WavePtrs&          Model::getAllShared<WavePtrs>();
+template Actions::Map&      Model::getAllShared<Actions::Map>();
+template ChannelSharedPtrs& Model::getAllShared<ChannelSharedPtrs>();
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T>
+T* Model::findShared(ID id)
+{
+#ifdef WITH_VST
+       if constexpr (std::is_same_v<T, Plugin>)
+               return get_(m_shared.plugins, id);
+#endif
+       if constexpr (std::is_same_v<T, Wave>)
+               return get_(m_shared.waves, id);
+
+       assert(false);
+}
+
+#ifdef WITH_VST
+template Plugin* Model::findShared<Plugin>(ID id);
+#endif
+template Wave* Model::findShared<Wave>(ID id);
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T>
+void Model::addShared(T obj)
+{
+#ifdef WITH_VST
+       if constexpr (std::is_same_v<T, PluginPtr>)
+               m_shared.plugins.push_back(std::move(obj));
+#endif
+       if constexpr (std::is_same_v<T, WavePtr>)
+               m_shared.waves.push_back(std::move(obj));
+       if constexpr (std::is_same_v<T, ChannelSharedPtr>)
+               m_shared.channelsShared.push_back(std::move(obj));
+}
+
+#ifdef WITH_VST
+template void Model::addShared<PluginPtr>(PluginPtr p);
+#endif
+template void Model::addShared<WavePtr>(WavePtr p);
+template void Model::addShared<ChannelSharedPtr>(ChannelSharedPtr p);
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T>
+void Model::removeShared(const T& ref)
+{
+#ifdef WITH_VST
+       if constexpr (std::is_same_v<T, Plugin>)
+               remove_(m_shared.plugins, ref);
+#endif
+       if constexpr (std::is_same_v<T, Wave>)
+               remove_(m_shared.waves, ref);
+}
+
+#ifdef WITH_VST
+template void Model::removeShared<Plugin>(const Plugin& t);
+#endif
+template void Model::removeShared<Wave>(const Wave& t);
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T>
+T& Model::backShared()
+{
+#ifdef WITH_VST
+       if constexpr (std::is_same_v<T, Plugin>)
+               return *m_shared.plugins.back().get();
+#endif
+       if constexpr (std::is_same_v<T, Wave>)
+               return *m_shared.waves.back().get();
+       if constexpr (std::is_same_v<T, ChannelShared>)
+               return *m_shared.channelsShared.back().get();
+}
+
+#ifdef WITH_VST
+template Plugin& Model::backShared<Plugin>();
+#endif
+template Wave&          Model::backShared<Wave>();
+template ChannelShared& Model::backShared<ChannelShared>();
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T>
+void Model::clearShared()
+{
+#ifdef WITH_VST
+       if constexpr (std::is_same_v<T, PluginPtrs>)
+               m_shared.plugins.clear();
+#endif
+       if constexpr (std::is_same_v<T, WavePtrs>)
+               m_shared.waves.clear();
+}
+
+#ifdef WITH_VST
+template void Model::clearShared<PluginPtrs>();
+#endif
+template void Model::clearShared<WavePtrs>();
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef G_DEBUG_MODE
+
+void Model::debug()
+{
+       puts("======== SYSTEM STATUS ========");
+
+       puts("model::layout.channels");
+
+       int i = 0;
+       for (const Channel& c : get().channels)
+       {
+               printf("\t%d) - ID=%d name='%s' type=%d columnID=%d shared=%p\n",
+                   i++, c.id, c.name.c_str(), (int)c.type, c.columnId, (void*)&c.shared);
+#ifdef WITH_VST
+               if (c.plugins.size() > 0)
+               {
+                       puts("\t\tplugins:");
+                       for (const auto& p : c.plugins)
+                               printf("\t\t\t%p - ID=%d\n", (void*)p, p->id);
+               }
+#endif
+       }
+
+       puts("model::state.channels");
+
+       i = 0;
+       for (const auto& c : m_shared.channelsShared)
+       {
+               printf("\t%d) - %p\n", i++, (void*)c.get());
+       }
+
+       puts("model::data.waves");
+
+       i = 0;
+       for (const auto& w : m_shared.waves)
+               printf("\t%d) %p - ID=%d name='%s'\n", i++, (void*)w.get(), w->id, w->getPath().c_str());
+
+       puts("model::data.actions");
+
+       for (const auto& [frame, actions] : getAllShared<Actions::Map>())
+       {
+               printf("\tframe: %d\n", frame);
+               for (const Action& a : actions)
+                       printf("\t\t(%p) - ID=%d, frame=%d, channel=%d, value=0x%X, prevId=%d, prev=%p, nextId=%d, next=%p\n",
+                           (void*)&a, a.id, a.frame, a.channelId, a.event.getRaw(), a.prevId, (void*)a.prev, a.nextId, (void*)a.next);
+       }
+
+#ifdef WITH_VST
+
+       puts("model::data.plugins");
+
+       i = 0;
+       for (const auto& p : m_shared.plugins)
+               printf("\t%d) %p - ID=%d\n", i++, (void*)p.get(), p->id);
+
+#endif
+}
+
+#endif // G_DEBUG_MODE
+} // namespace giada::m::model
diff --git a/src/core/model/model.h b/src/core/model/model.h
new file mode 100644 (file)
index 0000000..28b7c6b
--- /dev/null
@@ -0,0 +1,221 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MODEL_H
+#define G_MODEL_H
+
+#include "core/channels/channel.h"
+#include "core/const.h"
+#include "core/model/mixer.h"
+#include "core/model/recorder.h"
+#include "core/model/sequencer.h"
+#include "core/plugins/plugin.h"
+#include "core/recorder.h"
+#include "core/wave.h"
+#include "deps/mcl-atomic-swapper/src/atomic-swapper.hpp"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include "src/core/actions/actions.h"
+#include "utils/vector.h"
+#include <memory>
+
+namespace giada::m::model
+{
+struct MidiIn
+{
+       bool     enabled    = false;
+       int      filter     = -1;
+       uint32_t rewind     = 0x0;
+       uint32_t startStop  = 0x0;
+       uint32_t actionRec  = 0x0;
+       uint32_t inputRec   = 0x0;
+       uint32_t volumeIn   = 0x0;
+       uint32_t volumeOut  = 0x0;
+       uint32_t beatDouble = 0x0;
+       uint32_t beatHalf   = 0x0;
+       uint32_t metronome  = 0x0;
+};
+
+struct Layout
+{
+       Channel&       getChannel(ID id);
+       const Channel& getChannel(ID id) const;
+
+       Sequencer            sequencer;
+       Mixer                mixer;
+       Recorder             recorder;
+       MidiIn               midiIn;
+       std::vector<Channel> channels;
+
+       /* locked
+       If locked, Mixer won't process channels. This is used to allow editing the 
+       data (e.g. Actions or Plugins) a channel points to without data races. */
+
+       bool locked = false;
+};
+
+/* LayoutLock
+Alias for a REALTIME scoped lock provided by the Swapper class. Use this in the
+real-time thread to lock the Layout. */
+
+using LayoutLock = mcl::AtomicSwapper<Layout>::RtLock;
+
+/* SwapType
+Type of Layout change. 
+       Hard: the structure has changed (e.g. add a new channel);
+       Soft: a property has changed (e.g. change volume);
+       None: something has changed but we don't care. 
+Used by model listeners to determine the type of change that occurred in the 
+layout. */
+
+enum class SwapType
+{
+       HARD,
+       SOFT,
+       NONE
+};
+
+#ifdef WITH_VST
+using PluginPtr = std::unique_ptr<Plugin>;
+#endif
+using WavePtr          = std::unique_ptr<Wave>;
+using ChannelSharedPtr = std::unique_ptr<ChannelShared>;
+
+#ifdef WITH_VST
+using PluginPtrs = std::vector<PluginPtr>;
+#endif
+using WavePtrs          = std::vector<WavePtr>;
+using ChannelSharedPtrs = std::vector<ChannelSharedPtr>;
+
+/* -------------------------------------------------------------------------- */
+
+class DataLock;
+class Model
+{
+public:
+       Model();
+
+       bool isLocked() const;
+
+       /* lockData
+       Returns a scoped locker DataLock object. Use this when you want to lock
+       the model: a locked model won't be processed by Mixer. */
+
+       [[nodiscard]] DataLock lockData(SwapType t = SwapType::HARD);
+
+       /* reser
+       Resets the internal layout to default. */
+
+       void reset();
+
+       /* get_RT
+       Returns a LayoutLock object for REALTIME processing. Access layout by 
+       calling LayoutLock::get() method (returns ready-only Layout). */
+
+       LayoutLock get_RT();
+
+       /* get
+       Returns a reference to the NON-REALTIME layout structure. */
+
+       Layout&       get();
+       const Layout& get() const;
+
+       /* swap
+       Swap non-rt layout with the rt one. See 'SwapType' notes above. */
+
+       void swap(SwapType t);
+
+       template <typename T>
+       T& getAllShared();
+
+       /* findShared
+       Finds something in the shared data given an ID. Returns nullptr if the
+       object is not found. */
+
+       template <typename T>
+       T* findShared(ID id);
+
+       /* addShared
+       Adds some shared data (by moving it). */
+
+       template <typename T>
+       void addShared(T);
+
+       template <typename T>
+       void removeShared(const T&);
+
+       /* backShared
+       Returns a reference to the last added shared item. */
+
+       template <typename T>
+       T& backShared();
+
+       template <typename T>
+       void clearShared();
+
+#ifdef G_DEBUG_MODE
+       void debug();
+#endif
+
+       /* onSwap
+       Optional callback fired when the layout has been swapped. Useful for 
+       listening to model changes. */
+
+       std::function<void(SwapType)> onSwap = nullptr;
+
+private:
+       struct Shared
+       {
+               Sequencer::Shared                           sequencerShared;
+               Mixer::Shared                               mixerShared;
+               Recorder::Shared                            recorderShared;
+               std::vector<std::unique_ptr<ChannelShared>> channelsShared;
+
+               std::vector<std::unique_ptr<Wave>> waves;
+               Actions::Map                       actions;
+#ifdef WITH_VST
+               std::vector<std::unique_ptr<Plugin>> plugins;
+#endif
+       };
+
+       mcl::AtomicSwapper<Layout> m_layout;
+       Shared                     m_shared;
+};
+
+/* -------------------------------------------------------------------------- */
+
+class DataLock
+{
+public:
+       DataLock(Model&, SwapType t);
+       ~DataLock();
+
+private:
+       Model&   m_model;
+       SwapType m_swapType;
+};
+} // namespace giada::m::model
+
+#endif
diff --git a/src/core/model/recorder.cpp b/src/core/model/recorder.cpp
new file mode 100644 (file)
index 0000000..5d714fe
--- /dev/null
@@ -0,0 +1,50 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/model/recorder.h"
+
+namespace giada::m::model
+{
+bool Recorder::a_isRecordingAction() const
+{
+       return shared->isRecordingAction.load();
+}
+
+bool Recorder::a_isRecordingInput() const
+{
+       return shared->isRecordingInput.load();
+}
+
+void Recorder::a_setRecordingAction(bool b) const
+{
+       shared->isRecordingAction.store(b);
+}
+
+void Recorder::a_setRecordingInput(bool b) const
+{
+       shared->isRecordingInput.store(b);
+}
+} // namespace giada::m::model
diff --git a/src/core/model/recorder.h b/src/core/model/recorder.h
new file mode 100644 (file)
index 0000000..aca24e9
--- /dev/null
@@ -0,0 +1,55 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MODEL_RECORDER_H
+#define G_MODEL_RECORDER_H
+
+#include "core/weakAtomic.h"
+
+namespace giada::m::model
+{
+class Recorder
+{
+       friend class Model;
+
+public:
+       bool a_isRecordingAction() const;
+       bool a_isRecordingInput() const;
+       void a_setRecordingAction(bool) const;
+       void a_setRecordingInput(bool) const;
+
+private:
+       struct Shared
+       {
+               WeakAtomic<bool> isRecordingAction = false;
+               WeakAtomic<bool> isRecordingInput  = false;
+       };
+
+       Shared* shared = nullptr;
+};
+} // namespace giada::m::model
+
+#endif
diff --git a/src/core/model/sequencer.cpp b/src/core/model/sequencer.cpp
new file mode 100644 (file)
index 0000000..426f9d7
--- /dev/null
@@ -0,0 +1,84 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/model/sequencer.h"
+
+namespace giada::m::model
+{
+bool Sequencer::isActive() const
+{
+       return status == SeqStatus::RUNNING || status == SeqStatus::WAITING;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Sequencer::canQuantize() const
+{
+       return quantize > 0 && status == SeqStatus::RUNNING;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Sequencer::isRunning() const
+{
+       return status == SeqStatus::RUNNING;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Sequencer::a_isOnBar() const
+{
+       const int currentFrame = shared->currentFrame.load();
+
+       if (status == SeqStatus::WAITING || currentFrame == 0)
+               return false;
+       return currentFrame % framesInBar == 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Sequencer::a_isOnBeat() const
+{
+       return shared->currentFrame.load() % framesInBeat == 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Sequencer::a_isOnFirstBeat() const
+{
+       return shared->currentFrame.load() == 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame Sequencer::a_getCurrentFrame() const { return shared->currentFrame.load(); }
+Frame Sequencer::a_getCurrentBeat() const { return shared->currentBeat.load(); }
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::a_setCurrentFrame(Frame f) const { shared->currentFrame.store(f); }
+void Sequencer::a_setCurrentBeat(Frame f) const { shared->currentBeat.store(f); }
+} // namespace giada::m::model
diff --git a/src/core/model/sequencer.h b/src/core/model/sequencer.h
new file mode 100644 (file)
index 0000000..5389e86
--- /dev/null
@@ -0,0 +1,89 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MODEL_SEQUENCER_H
+#define G_MODEL_SEQUENCER_H
+
+#include "core/const.h"
+#include "core/types.h"
+#include "core/weakAtomic.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+
+namespace giada::m::model
+{
+class Sequencer
+{
+       friend class Model;
+
+public:
+       /* isRunning
+    When sequencer is actually moving forward, i.e. SeqStatus == RUNNING. */
+
+       bool isRunning() const;
+
+       /* isActive
+    Sequencer is enabled, but might be in wait mode, i.e. SeqStatus == RUNNING or
+    SeqStatus == WAITING. */
+
+       bool isActive() const;
+
+       /* canQuantize
+       Sequencer can quantize only if it's running and quantizer is enabled. */
+
+       bool canQuantize() const;
+
+       bool a_isOnBar() const;
+       bool a_isOnBeat() const;
+       bool a_isOnFirstBeat() const;
+
+       Frame a_getCurrentFrame() const;
+       Frame a_getCurrentBeat() const;
+
+       void a_setCurrentFrame(Frame) const;
+       void a_setCurrentBeat(Frame) const;
+
+       SeqStatus status       = SeqStatus::STOPPED;
+       int       framesInLoop = 0;
+       int       framesInBar  = 0;
+       int       framesInBeat = 0;
+       int       framesInSeq  = 0;
+       int       bars         = G_DEFAULT_BARS;
+       int       beats        = G_DEFAULT_BEATS;
+       float     bpm          = G_DEFAULT_BPM;
+       int       quantize     = G_DEFAULT_QUANTIZE;
+
+private:
+       struct Shared
+       {
+               WeakAtomic<Frame> currentFrame = 0;
+               WeakAtomic<Frame> currentBeat  = 0;
+       };
+
+       Shared* shared = nullptr;
+};
+} // namespace giada::m::model
+
+#endif
diff --git a/src/core/model/storage.cpp b/src/core/model/storage.cpp
new file mode 100644 (file)
index 0000000..1065b91
--- /dev/null
@@ -0,0 +1,188 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2019 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/model/storage.h"
+#include "core/channels/channelManager.h"
+#include "core/conf.h"
+#include "core/engine.h"
+#include "core/kernelAudio.h"
+#include "core/model/model.h"
+#include "core/patch.h"
+#include "core/plugins/pluginManager.h"
+#include "core/sequencer.h"
+#include "core/waveManager.h"
+#include "src/core/actions/actionRecorder.h"
+#include <cassert>
+#include <memory>
+
+extern giada::m::Engine g_engine;
+
+namespace giada::m::model
+{
+namespace
+{
+void loadChannels_(const std::vector<Patch::Channel>& channels, int samplerate)
+{
+       float samplerateRatio = g_engine.kernelAudio.getSampleRate() / static_cast<float>(samplerate);
+
+       for (const Patch::Channel& pchannel : channels)
+               g_engine.model.get().channels.push_back(
+                   g_engine.channelManager.deserializeChannel(pchannel, samplerateRatio, g_engine.kernelAudio.getBufferSize()));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void loadActions_(const std::vector<Patch::Action>& pactions)
+{
+       g_engine.model.getAllShared<Actions::Map>() = std::move(g_engine.actionRecorder.deserializeActions(pactions));
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+void store(Patch::Data& patch)
+{
+       const Layout& layout = g_engine.model.get();
+
+       patch.bars       = layout.sequencer.bars;
+       patch.beats      = layout.sequencer.beats;
+       patch.bpm        = layout.sequencer.bpm;
+       patch.quantize   = layout.sequencer.quantize;
+       patch.metronome  = g_engine.sequencer.isMetronomeOn(); // TODO - addShared bool metronome to Layout
+       patch.samplerate = g_engine.kernelAudio.getSampleRate();
+
+#ifdef WITH_VST
+       patch.plugins.clear();
+       for (const auto& p : g_engine.model.getAllShared<PluginPtrs>())
+               patch.plugins.push_back(g_engine.pluginManager.serializePlugin(*p));
+#endif
+
+       patch.actions = g_engine.actionRecorder.serializeActions(g_engine.model.getAllShared<Actions::Map>());
+
+       patch.waves.clear();
+       for (const auto& w : g_engine.model.getAllShared<WavePtrs>())
+               patch.waves.push_back(g_engine.waveManager.serializeWave(*w));
+
+       patch.channels.clear();
+       for (const Channel& c : layout.channels)
+               patch.channels.push_back(g_engine.channelManager.serializeChannel(c));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void store(Conf::Data& conf)
+{
+       const Layout& layout = g_engine.model.get();
+
+       conf.midiInEnabled    = layout.midiIn.enabled;
+       conf.midiInFilter     = layout.midiIn.filter;
+       conf.midiInRewind     = layout.midiIn.rewind;
+       conf.midiInStartStop  = layout.midiIn.startStop;
+       conf.midiInActionRec  = layout.midiIn.actionRec;
+       conf.midiInInputRec   = layout.midiIn.inputRec;
+       conf.midiInMetronome  = layout.midiIn.metronome;
+       conf.midiInVolumeIn   = layout.midiIn.volumeIn;
+       conf.midiInVolumeOut  = layout.midiIn.volumeOut;
+       conf.midiInBeatDouble = layout.midiIn.beatDouble;
+       conf.midiInBeatHalf   = layout.midiIn.beatHalf;
+}
+
+/* -------------------------------------------------------------------------- */
+
+LoadState load(const Patch::Data& patch)
+{
+       DataLock lock = g_engine.model.lockData(SwapType::NONE);
+
+       /* Clear and re-initialize channels first. */
+
+       g_engine.model.get().channels = {};
+       g_engine.model.getAllShared<ChannelSharedPtrs>().clear();
+
+       LoadState state;
+
+       /* Load external data first: plug-ins and waves. */
+
+#ifdef WITH_VST
+       g_engine.model.getAllShared<PluginPtrs>().clear();
+       for (const Patch::Plugin& pplugin : patch.plugins)
+       {
+               std::unique_ptr<Plugin> p = g_engine.pluginManager.deserializePlugin(
+                   pplugin, g_engine.kernelAudio.getSampleRate(), g_engine.kernelAudio.getBufferSize(), g_engine.sequencer);
+
+               if (!p->valid)
+                       state.missingPlugins.push_back(pplugin.path);
+
+               g_engine.model.getAllShared<PluginPtrs>().push_back(std::move(p));
+       }
+#endif
+
+       g_engine.model.getAllShared<WavePtrs>().clear();
+       for (const Patch::Wave& pwave : patch.waves)
+       {
+               std::unique_ptr<Wave> w = g_engine.waveManager.deserializeWave(pwave, g_engine.kernelAudio.getSampleRate(),
+                   g_engine.conf.data.rsmpQuality);
+
+               if (w != nullptr)
+                       g_engine.model.getAllShared<WavePtrs>().push_back(std::move(w));
+               else
+                       state.missingWaves.push_back(pwave.path);
+       }
+
+       /* Then load up channels, actions and global properties. */
+
+       loadChannels_(patch.channels, g_engine.patch.data.samplerate);
+       loadActions_(patch.actions);
+
+       g_engine.model.get().sequencer.status   = SeqStatus::STOPPED;
+       g_engine.model.get().sequencer.bars     = patch.bars;
+       g_engine.model.get().sequencer.beats    = patch.beats;
+       g_engine.model.get().sequencer.bpm      = patch.bpm;
+       g_engine.model.get().sequencer.quantize = patch.quantize;
+
+       return state;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void load(const Conf::Data& c)
+{
+       g_engine.model.get().midiIn.enabled    = c.midiInEnabled;
+       g_engine.model.get().midiIn.filter     = c.midiInFilter;
+       g_engine.model.get().midiIn.rewind     = c.midiInRewind;
+       g_engine.model.get().midiIn.startStop  = c.midiInStartStop;
+       g_engine.model.get().midiIn.actionRec  = c.midiInActionRec;
+       g_engine.model.get().midiIn.inputRec   = c.midiInInputRec;
+       g_engine.model.get().midiIn.volumeIn   = c.midiInVolumeIn;
+       g_engine.model.get().midiIn.volumeOut  = c.midiInVolumeOut;
+       g_engine.model.get().midiIn.beatDouble = c.midiInBeatDouble;
+       g_engine.model.get().midiIn.beatHalf   = c.midiInBeatHalf;
+       g_engine.model.get().midiIn.metronome  = c.midiInMetronome;
+
+       g_engine.model.swap(SwapType::NONE);
+}
+} // namespace giada::m::model
diff --git a/src/core/model/storage.h b/src/core/model/storage.h
new file mode 100644 (file)
index 0000000..aa1ce01
--- /dev/null
@@ -0,0 +1,42 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2019 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MODEL_STORAGE_H
+#define G_MODEL_STORAGE_H
+
+#include "core/conf.h"
+#include "core/engine.h"
+#include "core/patch.h"
+
+namespace giada::m::model
+{
+void      store(Conf::Data& c);
+void      store(Patch::Data& p);
+LoadState load(const Patch::Data& p);
+void      load(const Conf::Data& c);
+} // namespace giada::m::model
+
+#endif
diff --git a/src/core/patch.cpp b/src/core/patch.cpp
new file mode 100644 (file)
index 0000000..498b12f
--- /dev/null
@@ -0,0 +1,476 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "patch.h"
+#include "core/mixer.h"
+#include "deps/json/single_include/nlohmann/json.hpp"
+#include "utils/log.h"
+#include "utils/math.h"
+#include <fstream>
+
+namespace nl = nlohmann;
+
+namespace giada::m
+{
+namespace
+{
+void readCommons_(Patch::Data& patch, const nl::json& j)
+{
+       patch.name       = j.value(PATCH_KEY_NAME, G_DEFAULT_PATCH_NAME);
+       patch.bars       = j.value(PATCH_KEY_BARS, G_DEFAULT_BARS);
+       patch.beats      = j.value(PATCH_KEY_BEATS, G_DEFAULT_BEATS);
+       patch.bpm        = j.value(PATCH_KEY_BPM, G_DEFAULT_BPM);
+       patch.quantize   = j.value(PATCH_KEY_QUANTIZE, G_DEFAULT_QUANTIZE);
+       patch.lastTakeId = j.value(PATCH_KEY_LAST_TAKE_ID, 0);
+       patch.samplerate = j.value(PATCH_KEY_SAMPLERATE, G_DEFAULT_SAMPLERATE);
+       patch.metronome  = j.value(PATCH_KEY_METRONOME, false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void readColumns_(Patch::Data& patch, const nl::json& j)
+{
+       ID id = 0;
+       for (const auto& jcol : j[PATCH_KEY_COLUMNS])
+       {
+               Patch::Column c;
+               c.id    = jcol.value(PATCH_KEY_COLUMN_ID, ++id);
+               c.width = jcol.value(PATCH_KEY_COLUMN_WIDTH, G_DEFAULT_COLUMN_WIDTH);
+               patch.columns.push_back(c);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void readPlugins_(Patch::Data& patch, const nl::json& j)
+{
+       if (!j.contains(PATCH_KEY_PLUGINS))
+               return;
+
+       ID id = 0;
+       for (const auto& jplugin : j[PATCH_KEY_PLUGINS])
+       {
+               Patch::Plugin p;
+               p.id     = jplugin.value(PATCH_KEY_PLUGIN_ID, ++id);
+               p.path   = jplugin.value(PATCH_KEY_PLUGIN_PATH, "");
+               p.bypass = jplugin.value(PATCH_KEY_PLUGIN_BYPASS, false);
+
+               if (patch.version < Patch::Version{0, 17, 0})
+                       for (const auto& jparam : jplugin[PATCH_KEY_PLUGIN_PARAMS])
+                               p.params.push_back(jparam);
+               else
+                       p.state = jplugin.value(PATCH_KEY_PLUGIN_STATE, "");
+
+               for (const auto& jmidiParam : jplugin[PATCH_KEY_PLUGIN_MIDI_IN_PARAMS])
+                       p.midiInParams.push_back(jmidiParam);
+
+               patch.plugins.push_back(p);
+       }
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void readWaves_(Patch::Data& patch, const nl::json& j, const std::string& basePath)
+{
+       if (!j.contains(PATCH_KEY_WAVES))
+               return;
+
+       ID id = 0;
+       for (const auto& jwave : j[PATCH_KEY_WAVES])
+       {
+               Patch::Wave w;
+               w.id   = jwave.value(PATCH_KEY_WAVE_ID, ++id);
+               w.path = basePath + G_SLASH + jwave.value(PATCH_KEY_WAVE_PATH, "");
+               patch.waves.push_back(w);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void readActions_(Patch::Data& patch, const nl::json& j)
+{
+       if (!j.contains(PATCH_KEY_ACTIONS))
+               return;
+
+       ID id = 0;
+       for (const auto& jaction : j[PATCH_KEY_ACTIONS])
+       {
+               Patch::Action a;
+               a.id        = jaction.value(G_PATCH_KEY_ACTION_ID, ++id);
+               a.channelId = jaction.value(G_PATCH_KEY_ACTION_CHANNEL, 0);
+               a.frame     = jaction.value(G_PATCH_KEY_ACTION_FRAME, 0);
+               a.event     = jaction.value(G_PATCH_KEY_ACTION_EVENT, 0);
+               a.prevId    = jaction.value(G_PATCH_KEY_ACTION_PREV, 0);
+               a.nextId    = jaction.value(G_PATCH_KEY_ACTION_NEXT, 0);
+               patch.actions.push_back(a);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void readChannels_(Patch::Data& patch, const nl::json& j)
+{
+       if (!j.contains(PATCH_KEY_CHANNELS))
+               return;
+
+       ID defaultId = Mixer::PREVIEW_CHANNEL_ID;
+
+       for (const auto& jchannel : j[PATCH_KEY_CHANNELS])
+       {
+               Patch::Channel c;
+               c.id                = jchannel.value(PATCH_KEY_CHANNEL_ID, ++defaultId);
+               c.type              = static_cast<ChannelType>(jchannel.value(PATCH_KEY_CHANNEL_TYPE, 1));
+               c.volume            = jchannel.value(PATCH_KEY_CHANNEL_VOLUME, G_DEFAULT_VOL);
+               c.height            = jchannel.value(PATCH_KEY_CHANNEL_SIZE, G_GUI_UNIT);
+               c.name              = jchannel.value(PATCH_KEY_CHANNEL_NAME, "");
+               c.columnId          = jchannel.value(PATCH_KEY_CHANNEL_COLUMN, 1);
+               c.key               = jchannel.value(PATCH_KEY_CHANNEL_KEY, 0);
+               c.mute              = jchannel.value(PATCH_KEY_CHANNEL_MUTE, 0);
+               c.solo              = jchannel.value(PATCH_KEY_CHANNEL_SOLO, 0);
+               c.pan               = jchannel.value(PATCH_KEY_CHANNEL_PAN, 0.5f);
+               c.hasActions        = jchannel.value(PATCH_KEY_CHANNEL_HAS_ACTIONS, false);
+               c.midiIn            = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN, 0);
+               c.midiInKeyPress    = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_KEYPRESS, 0);
+               c.midiInKeyRel      = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_KEYREL, 0);
+               c.midiInKill        = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_KILL, 0);
+               c.midiInArm         = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_ARM, 0);
+               c.midiInVolume      = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_VOLUME, 0);
+               c.midiInMute        = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_MUTE, 0);
+               c.midiInSolo        = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_SOLO, 0);
+               c.midiInFilter      = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_FILTER, 0);
+               c.midiOutL          = jchannel.value(PATCH_KEY_CHANNEL_MIDI_OUT_L, 0);
+               c.midiOutLplaying   = jchannel.value(PATCH_KEY_CHANNEL_MIDI_OUT_L_PLAYING, 0);
+               c.midiOutLmute      = jchannel.value(PATCH_KEY_CHANNEL_MIDI_OUT_L_MUTE, 0);
+               c.midiOutLsolo      = jchannel.value(PATCH_KEY_CHANNEL_MIDI_OUT_L_SOLO, 0);
+               c.armed             = jchannel.value(PATCH_KEY_CHANNEL_ARMED, false);
+               c.mode              = static_cast<SamplePlayerMode>(jchannel.value(PATCH_KEY_CHANNEL_MODE, 1));
+               c.waveId            = jchannel.value(PATCH_KEY_CHANNEL_WAVE_ID, 0);
+               c.begin             = jchannel.value(PATCH_KEY_CHANNEL_BEGIN, 0);
+               c.end               = jchannel.value(PATCH_KEY_CHANNEL_END, 0);
+               c.shift             = jchannel.value(PATCH_KEY_CHANNEL_SHIFT, 0);
+               c.readActions       = jchannel.value(PATCH_KEY_CHANNEL_READ_ACTIONS, false);
+               c.pitch             = jchannel.value(PATCH_KEY_CHANNEL_PITCH, G_DEFAULT_PITCH);
+               c.inputMonitor      = jchannel.value(PATCH_KEY_CHANNEL_INPUT_MONITOR, false);
+               c.overdubProtection = jchannel.value(PATCH_KEY_CHANNEL_OVERDUB_PROTECTION, false);
+               c.midiInVeloAsVol   = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_VELO_AS_VOL, 0);
+               c.midiInReadActions = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_READ_ACTIONS, 0);
+               c.midiInPitch       = jchannel.value(PATCH_KEY_CHANNEL_MIDI_IN_PITCH, 0);
+               c.midiOut           = jchannel.value(PATCH_KEY_CHANNEL_MIDI_OUT, 0);
+               c.midiOutChan       = jchannel.value(PATCH_KEY_CHANNEL_MIDI_OUT_CHAN, 0);
+
+#ifdef WITH_VST
+               if (jchannel.contains(PATCH_KEY_CHANNEL_PLUGINS))
+                       for (const auto& jplugin : jchannel[PATCH_KEY_CHANNEL_PLUGINS])
+                               c.pluginIds.push_back(jplugin);
+#endif
+
+               patch.channels.push_back(c);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void writePlugins_(const Patch::Data& patch, nl::json& j)
+{
+       j[PATCH_KEY_PLUGINS] = nl::json::array();
+
+       for (const Patch::Plugin& p : patch.plugins)
+       {
+
+               nl::json jplugin;
+
+               jplugin[PATCH_KEY_PLUGIN_ID]     = p.id;
+               jplugin[PATCH_KEY_PLUGIN_PATH]   = p.path;
+               jplugin[PATCH_KEY_PLUGIN_BYPASS] = p.bypass;
+               jplugin[PATCH_KEY_PLUGIN_STATE]  = p.state;
+
+               jplugin[PATCH_KEY_PLUGIN_MIDI_IN_PARAMS] = nl::json::array();
+               for (uint32_t param : p.midiInParams)
+                       jplugin[PATCH_KEY_PLUGIN_MIDI_IN_PARAMS].push_back(param);
+
+               j[PATCH_KEY_PLUGINS].push_back(jplugin);
+       }
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void writeColumns_(const Patch::Data& patch, nl::json& j)
+{
+       j[PATCH_KEY_COLUMNS] = nl::json::array();
+
+       for (const Patch::Column& column : patch.columns)
+       {
+               nl::json jcolumn;
+               jcolumn[PATCH_KEY_COLUMN_ID]    = column.id;
+               jcolumn[PATCH_KEY_COLUMN_WIDTH] = column.width;
+               j[PATCH_KEY_COLUMNS].push_back(jcolumn);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void writeActions_(const Patch::Data& patch, nl::json& j)
+{
+       j[PATCH_KEY_ACTIONS] = nl::json::array();
+
+       for (const Patch::Action& a : patch.actions)
+       {
+               nl::json jaction;
+               jaction[G_PATCH_KEY_ACTION_ID]      = a.id;
+               jaction[G_PATCH_KEY_ACTION_CHANNEL] = a.channelId;
+               jaction[G_PATCH_KEY_ACTION_FRAME]   = a.frame;
+               jaction[G_PATCH_KEY_ACTION_EVENT]   = a.event;
+               jaction[G_PATCH_KEY_ACTION_PREV]    = a.prevId;
+               jaction[G_PATCH_KEY_ACTION_NEXT]    = a.nextId;
+               j[PATCH_KEY_ACTIONS].push_back(jaction);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void writeWaves_(const Patch::Data& patch, nl::json& j)
+{
+       j[PATCH_KEY_WAVES] = nl::json::array();
+
+       for (const Patch::Wave& w : patch.waves)
+       {
+               nl::json jwave;
+               jwave[PATCH_KEY_WAVE_ID]   = w.id;
+               jwave[PATCH_KEY_WAVE_PATH] = w.path;
+
+               j[PATCH_KEY_WAVES].push_back(jwave);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void writeCommons_(const Patch::Data& patch, nl::json& j)
+{
+       j[PATCH_KEY_HEADER]        = "GIADAPTC";
+       j[PATCH_KEY_VERSION_MAJOR] = G_VERSION_MAJOR;
+       j[PATCH_KEY_VERSION_MINOR] = G_VERSION_MINOR;
+       j[PATCH_KEY_VERSION_PATCH] = G_VERSION_PATCH;
+       j[PATCH_KEY_NAME]          = patch.name;
+       j[PATCH_KEY_BARS]          = patch.bars;
+       j[PATCH_KEY_BEATS]         = patch.beats;
+       j[PATCH_KEY_BPM]           = patch.bpm;
+       j[PATCH_KEY_QUANTIZE]      = patch.quantize;
+       j[PATCH_KEY_LAST_TAKE_ID]  = patch.lastTakeId;
+       j[PATCH_KEY_SAMPLERATE]    = patch.samplerate;
+       j[PATCH_KEY_METRONOME]     = patch.metronome;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void writeChannels_(const Patch::Data& patch, nl::json& j)
+{
+       j[PATCH_KEY_CHANNELS] = nl::json::array();
+
+       for (const Patch::Channel& c : patch.channels)
+       {
+               nl::json jchannel;
+
+               jchannel[PATCH_KEY_CHANNEL_ID]                   = c.id;
+               jchannel[PATCH_KEY_CHANNEL_TYPE]                 = static_cast<int>(c.type);
+               jchannel[PATCH_KEY_CHANNEL_SIZE]                 = c.height;
+               jchannel[PATCH_KEY_CHANNEL_NAME]                 = c.name;
+               jchannel[PATCH_KEY_CHANNEL_COLUMN]               = c.columnId;
+               jchannel[PATCH_KEY_CHANNEL_MUTE]                 = c.mute;
+               jchannel[PATCH_KEY_CHANNEL_SOLO]                 = c.solo;
+               jchannel[PATCH_KEY_CHANNEL_VOLUME]               = c.volume;
+               jchannel[PATCH_KEY_CHANNEL_PAN]                  = c.pan;
+               jchannel[PATCH_KEY_CHANNEL_HAS_ACTIONS]          = c.hasActions;
+               jchannel[PATCH_KEY_CHANNEL_ARMED]                = c.armed;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN]              = c.midiIn;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_KEYREL]       = c.midiInKeyRel;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_KEYPRESS]     = c.midiInKeyPress;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_KILL]         = c.midiInKill;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_ARM]          = c.midiInArm;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_VOLUME]       = c.midiInVolume;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_MUTE]         = c.midiInMute;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_SOLO]         = c.midiInSolo;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_FILTER]       = c.midiInFilter;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_OUT_L]           = c.midiOutL;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_OUT_L_PLAYING]   = c.midiOutLplaying;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_OUT_L_MUTE]      = c.midiOutLmute;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_OUT_L_SOLO]      = c.midiOutLsolo;
+               jchannel[PATCH_KEY_CHANNEL_KEY]                  = c.key;
+               jchannel[PATCH_KEY_CHANNEL_WAVE_ID]              = c.waveId;
+               jchannel[PATCH_KEY_CHANNEL_MODE]                 = static_cast<int>(c.mode);
+               jchannel[PATCH_KEY_CHANNEL_BEGIN]                = c.begin;
+               jchannel[PATCH_KEY_CHANNEL_END]                  = c.end;
+               jchannel[PATCH_KEY_CHANNEL_SHIFT]                = c.shift;
+               jchannel[PATCH_KEY_CHANNEL_READ_ACTIONS]         = c.readActions;
+               jchannel[PATCH_KEY_CHANNEL_PITCH]                = c.pitch;
+               jchannel[PATCH_KEY_CHANNEL_INPUT_MONITOR]        = c.inputMonitor;
+               jchannel[PATCH_KEY_CHANNEL_OVERDUB_PROTECTION]   = c.overdubProtection;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_VELO_AS_VOL]  = c.midiInVeloAsVol;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_READ_ACTIONS] = c.midiInReadActions;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_IN_PITCH]        = c.midiInPitch;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_OUT]             = c.midiOut;
+               jchannel[PATCH_KEY_CHANNEL_MIDI_OUT_CHAN]        = c.midiOutChan;
+
+#ifdef WITH_VST
+               jchannel[PATCH_KEY_CHANNEL_PLUGINS] = nl::json::array();
+               for (ID pid : c.pluginIds)
+                       jchannel[PATCH_KEY_CHANNEL_PLUGINS].push_back(pid);
+#endif
+
+               j[PATCH_KEY_CHANNELS].push_back(jchannel);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void modernize_(Patch::Data& patch)
+{
+       for (Patch::Channel& c : patch.channels)
+       {
+               /* 0.16.3
+               Make sure that ChannelType is correct: ID 1, 2 are MASTER channels, ID 3 
+               is PREVIEW channel. */
+               if (c.id == Mixer::MASTER_OUT_CHANNEL_ID || c.id == Mixer::MASTER_IN_CHANNEL_ID)
+                       c.type = ChannelType::MASTER;
+               else if (c.id == Mixer::PREVIEW_CHANNEL_ID)
+                       c.type = ChannelType::PREVIEW;
+
+               /* 0.16.4
+               Make sure internal channels are never armed. */
+               if (c.type == ChannelType::PREVIEW || c.type == ChannelType::MASTER)
+                       c.armed = false;
+
+               /* 0.16.3
+               Set panning to default (0.5) and waveId to 0 for non-Sample Channels. */
+               if (c.type != ChannelType::SAMPLE)
+               {
+                       c.pan    = G_DEFAULT_PAN;
+                       c.waveId = 0;
+               }
+       }
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+bool Patch::Version::operator==(const Version& o) const
+{
+       return major == o.major && minor == o.minor && patch == o.patch;
+}
+
+bool Patch::Version::operator<(const Version& o) const
+{
+       if (major < o.major)
+               return true;
+       if (minor < o.minor)
+               return true;
+       if (patch < o.patch)
+               return true;
+       return false;
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+void Patch::reset()
+{
+       data = Data();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Patch::write(const std::string& file) const
+{
+       nl::json j;
+
+       writeCommons_(data, j);
+       writeColumns_(data, j);
+       writeChannels_(data, j);
+       writeActions_(data, j);
+       writeWaves_(data, j);
+#ifdef WITH_VST
+       writePlugins_(data, j);
+#endif
+
+       std::ofstream ofs(file);
+       if (!ofs.good())
+               return false;
+
+       ofs << j;
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int Patch::read(const std::string& file, const std::string& basePath)
+{
+       std::ifstream ifs(file);
+       if (!ifs.good())
+               return G_PATCH_UNREADABLE;
+
+       nl::json j = nl::json::parse(ifs);
+
+       if (j[PATCH_KEY_HEADER] != "GIADAPTC")
+               return G_PATCH_INVALID;
+
+       data.version = {
+           static_cast<int>(j[PATCH_KEY_VERSION_MAJOR]),
+           static_cast<int>(j[PATCH_KEY_VERSION_MINOR]),
+           static_cast<int>(j[PATCH_KEY_VERSION_PATCH])};
+       if (data.version < Version{0, 16, 0})
+               return G_PATCH_UNSUPPORTED;
+
+       try
+       {
+               readCommons_(data, j);
+               readColumns_(data, j);
+#ifdef WITH_VST
+               readPlugins_(data, j);
+#endif
+               readWaves_(data, j, basePath);
+               readActions_(data, j);
+               readChannels_(data, j);
+               modernize_(data);
+       }
+       catch (nl::json::exception& e)
+       {
+               u::log::print("[patch::read] Exception thrown: %s\n", e.what());
+               return G_PATCH_INVALID;
+       }
+
+       return G_PATCH_OK;
+}
+} // namespace giada::m
diff --git a/src/core/patch.h b/src/core/patch.h
new file mode 100644 (file)
index 0000000..f359251
--- /dev/null
@@ -0,0 +1,173 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_PATCH_H
+#define G_PATCH_H
+
+#include "core/const.h"
+#include "core/types.h"
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace giada::m
+{
+class Patch
+{
+public:
+       struct Version
+       {
+               int major = G_VERSION_MAJOR;
+               int minor = G_VERSION_MINOR;
+               int patch = G_VERSION_PATCH;
+
+               bool operator==(const Version& o) const;
+               bool operator<(const Version& o) const;
+       };
+
+       struct Column
+       {
+               ID  id;
+               int width;
+       };
+
+       struct Channel
+       {
+               ID          id;
+               ChannelType type;
+               int         height;
+               std::string name;
+               ID          columnId;
+               int         key;
+               bool        mute;
+               bool        solo;
+               float       volume = G_DEFAULT_VOL;
+               float       pan    = G_DEFAULT_PAN;
+               bool        hasActions;
+               bool        armed;
+               bool        midiIn;
+               uint32_t    midiInKeyPress;
+               uint32_t    midiInKeyRel;
+               uint32_t    midiInKill;
+               uint32_t    midiInArm;
+               uint32_t    midiInVolume;
+               uint32_t    midiInMute;
+               uint32_t    midiInSolo;
+               int         midiInFilter;
+               bool        midiOutL;
+               uint32_t    midiOutLplaying;
+               uint32_t    midiOutLmute;
+               uint32_t    midiOutLsolo;
+               // sample channel
+               ID               waveId = 0;
+               SamplePlayerMode mode;
+               Frame            begin;
+               Frame            end;
+               Frame            shift;
+               bool             readActions;
+               float            pitch = G_DEFAULT_PITCH;
+               bool             inputMonitor;
+               bool             overdubProtection;
+               bool             midiInVeloAsVol;
+               uint32_t         midiInReadActions;
+               uint32_t         midiInPitch;
+               // midi channel
+               bool midiOut;
+               int  midiOutChan;
+#ifdef WITH_VST
+               std::vector<ID> pluginIds;
+#endif
+       };
+
+       struct Action
+       {
+               ID       id;
+               ID       channelId;
+               Frame    frame;
+               uint32_t event;
+               ID       prevId;
+               ID       nextId;
+       };
+
+       struct Wave
+       {
+               ID          id;
+               std::string path;
+       };
+
+#ifdef WITH_VST
+       struct Plugin
+       {
+               ID                    id;
+               std::string           path;
+               bool                  bypass;
+               std::vector<float>    params; // TODO - to be removed in 0.18.0
+               std::string           state;
+               std::vector<uint32_t> midiInParams;
+       };
+#endif
+
+       struct Data
+       {
+               Version     version;
+               std::string name       = G_DEFAULT_PATCH_NAME;
+               int         bars       = G_DEFAULT_BARS;
+               int         beats      = G_DEFAULT_BEATS;
+               float       bpm        = G_DEFAULT_BPM;
+               bool        quantize   = G_DEFAULT_QUANTIZE;
+               int         lastTakeId = 0;
+               int         samplerate = G_DEFAULT_SAMPLERATE;
+               bool        metronome  = false;
+
+               std::vector<Column>  columns;
+               std::vector<Channel> channels;
+               std::vector<Action>  actions;
+               std::vector<Wave>    waves;
+#ifdef WITH_VST
+               std::vector<Plugin> plugins;
+#endif
+       };
+
+       /* write
+       Writes patch to file. */
+
+       bool write(const std::string& file) const;
+
+       /* reset
+       Initializes the patch with default values. */
+
+       void reset();
+
+       /* read
+       Reads patch from file. It takes 'basePath' as parameter for Wave reading. */
+
+       int read(const std::string& file, const std::string& basePath);
+
+       Data data;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/plugins/plugin.cpp b/src/core/plugins/plugin.cpp
new file mode 100644 (file)
index 0000000..23e1dde
--- /dev/null
@@ -0,0 +1,304 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "core/plugins/plugin.h"
+#include "core/const.h"
+#include "utils/log.h"
+#include "utils/time.h"
+#include <FL/Fl.H>
+#include <cassert>
+#include <memory>
+
+namespace giada::m
+{
+Plugin::Plugin(ID id, const std::string& UID)
+: id(id)
+, valid(false)
+, onEditorResize(nullptr)
+, m_plugin(nullptr)
+, m_UID(UID)
+, m_hasEditor(false)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+Plugin::Plugin(ID id, std::unique_ptr<juce::AudioPluginInstance> plugin,
+    std::unique_ptr<PluginHost::Info> playHead, double samplerate, int buffersize)
+: id(id)
+, valid(true)
+, onEditorResize(nullptr)
+, m_plugin(std::move(plugin))
+, m_playHead(std::move(playHead))
+, m_bypass(false)
+, m_hasEditor(m_plugin->hasEditor())
+{
+       /* (1) Initialize midiInParams vector, where midiInParams.size == number of 
+       plugin parameters. All values are initially empty (0x0): they will be filled
+       during MIDI learning process. */
+
+       for (int i = 0; i < m_plugin->getParameters().size(); i++)
+               midiInParams.emplace_back(0x0, i);
+
+       m_buffer.setSize(G_MAX_IO_CHANS, buffersize);
+
+       /* Try to set the main bus to the current number of channels. In the future
+       this setup will be performed manually through a proper channel matrix. */
+
+       juce::AudioProcessor::Bus* outBus = getMainBus(BusType::OUT);
+       juce::AudioProcessor::Bus* inBus  = getMainBus(BusType::IN);
+       if (outBus != nullptr)
+               outBus->setNumberOfChannels(G_MAX_IO_CHANS);
+       if (inBus != nullptr)
+               inBus->setNumberOfChannels(G_MAX_IO_CHANS);
+
+       /* Set pointer to PlayHead, used to pass Giada information (bpm, time, ...)
+       to the plug-in. */
+
+       m_plugin->setPlayHead(m_playHead.get());
+
+       m_plugin->prepareToPlay(samplerate, buffersize);
+
+       u::log::print("[Plugin] plugin initialized and ready. MIDI input params: %lu\n",
+           midiInParams.size());
+}
+
+/* -------------------------------------------------------------------------- */
+
+Plugin::~Plugin()
+{
+       if (!valid)
+               return;
+
+       juce::AudioProcessorEditor* e = m_plugin->getActiveEditor();
+       if (e != nullptr)
+               e->removeComponentListener(this);
+
+       m_plugin->suspendProcessing(true);
+       m_plugin->releaseResources();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Plugin::componentMovedOrResized(juce::Component& c, bool moved, bool /* resized*/)
+{
+       if (moved)
+               return;
+       if (onEditorResize != nullptr)
+               onEditorResize(c.getWidth(), c.getHeight());
+}
+
+/* -------------------------------------------------------------------------- */
+
+juce::AudioProcessor::Bus* Plugin::getMainBus(BusType b) const
+{
+       const bool isInput = static_cast<bool>(b);
+       for (int i = 0; i < m_plugin->getBusCount(isInput); i++)
+               if (m_plugin->getBus(isInput, i)->isMain())
+                       return m_plugin->getBus(isInput, i);
+       return nullptr;
+}
+
+int Plugin::countMainOutChannels() const
+{
+       juce::AudioProcessor::Bus* b = getMainBus(BusType::OUT);
+       assert(b != nullptr);
+       return b->getNumberOfChannels();
+}
+
+/* -------------------------------------------------------------------------- */
+
+juce::AudioProcessorEditor* Plugin::createEditor() const
+{
+       juce::AudioProcessorEditor* e = m_plugin->createEditorIfNeeded();
+       if (e != nullptr)
+               e->addComponentListener(const_cast<Plugin*>(this));
+       return e;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string Plugin::getUniqueId() const
+{
+       if (!valid)
+               return m_UID;
+       return m_plugin->getPluginDescription().createIdentifierString().toStdString();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int Plugin::getNumParameters() const
+{
+       return valid ? m_plugin->getParameters().size() : 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+float Plugin::getParameter(int paramIndex) const
+{
+       return m_plugin->getParameters()[paramIndex]->getValue();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Plugin::setParameter(int paramIndex, float value) const
+{
+       m_plugin->getParameters()[paramIndex]->setValue(value);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string Plugin::getName() const
+{
+       if (!valid)
+               return "** invalid **";
+       return m_plugin->getName().toStdString();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Plugin::isSuspended() const
+{
+       if (!valid)
+               return false;
+       return m_plugin->isSuspended();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Plugin::isInstrument() const
+{
+       if (!valid)
+               return false;
+       return m_plugin->acceptsMidi() && m_plugin->getTotalNumInputChannels() == 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+PluginState Plugin::getState() const
+{
+       if (!valid)
+               return {};
+       juce::MemoryBlock data;
+       m_plugin->getStateInformation(data);
+       return PluginState(std::move(data));
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Plugin::isBypassed() const { return m_bypass.load(); }
+void Plugin::setBypass(bool b) { m_bypass.store(b); }
+
+/* -------------------------------------------------------------------------- */
+
+const Plugin::Buffer& Plugin::process(const Plugin::Buffer& out, juce::MidiBuffer m)
+{
+       /* Copy the incoming buffer data into the temporary one. This way FXes will 
+       process existing audio data on the private buffer. This is needed later on
+       when merging it back into the incoming buffer. */
+
+       m_buffer = out;
+       m_plugin->processBlock(m_buffer, m);
+       return m_buffer;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Plugin::setState(PluginState state)
+{
+       m_plugin->setStateInformation(state.getData(), state.getSize());
+}
+
+/* -------------------------------------------------------------------------- */
+
+int Plugin::getNumPrograms() const
+{
+       if (!valid)
+               return 0;
+       return m_plugin->getNumPrograms();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int Plugin::getCurrentProgram() const
+{
+       if (!valid)
+               return 0;
+       return m_plugin->getCurrentProgram();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Plugin::setCurrentProgram(int index) const
+{
+       if (valid)
+               m_plugin->setCurrentProgram(index);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Plugin::hasEditor() const
+{
+       return m_hasEditor;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string Plugin::getProgramName(int index) const
+{
+       if (!valid)
+               return {};
+       return m_plugin->getProgramName(index).toStdString();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string Plugin::getParameterName(int index) const
+{
+       if (!valid)
+               return {};
+       const int labelSize = 64;
+       return m_plugin->getParameters()[index]->getName(labelSize).toStdString();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string Plugin::getParameterText(int index) const
+{
+       return m_plugin->getParameters()[index]->getCurrentValueAsText().toStdString();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string Plugin::getParameterLabel(int index) const
+{
+       return m_plugin->getParameters()[index]->getLabel().toStdString();
+}
+} // namespace giada::m
+
+#endif
diff --git a/src/core/plugins/plugin.h b/src/core/plugins/plugin.h
new file mode 100644 (file)
index 0000000..0609327
--- /dev/null
@@ -0,0 +1,162 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef G_PLUGIN_H
+#define G_PLUGIN_H
+
+#include "core/const.h"
+#include "core/midiLearnParam.h"
+#include "core/plugins/pluginHost.h"
+#include "core/plugins/pluginState.h"
+#include "deps/juce-config.h"
+#include <memory>
+#include <vector>
+
+namespace giada::m
+{
+class Plugin : private juce::ComponentListener
+{
+public:
+       using Buffer = juce::AudioBuffer<float>;
+
+       /* Plugin (1)
+       Constructs an invalid plug-in. */
+
+       Plugin(ID id, const std::string& UID);
+
+       /* Plugin (2)
+       Constructs a valid and working plug-in. */
+
+       Plugin(ID  id, std::unique_ptr<juce::AudioPluginInstance>, std::unique_ptr<PluginHost::Info>,
+           double samplerate, int buffersize);
+
+       Plugin(const Plugin& o) = delete;
+       Plugin(Plugin&& o)      = delete;
+       Plugin& operator=(const Plugin&) = delete;
+       Plugin& operator=(Plugin&&) = delete;
+
+       ~Plugin();
+
+       /* getUniqueId
+       Returns a string-based UID. */
+
+       std::string                 getUniqueId() const;
+       std::string                 getName() const;
+       bool                        hasEditor() const;
+       int                         getNumParameters() const;
+       float                       getParameter(int index) const;
+       std::string                 getParameterName(int index) const;
+       std::string                 getParameterText(int index) const;
+       std::string                 getParameterLabel(int index) const;
+       bool                        isSuspended() const;
+       bool                        isBypassed() const;
+       bool                        isInstrument() const;
+       int                         getNumPrograms() const;
+       int                         getCurrentProgram() const;
+       std::string                 getProgramName(int index) const;
+       void                        setParameter(int index, float value) const;
+       void                        setCurrentProgram(int index) const;
+       PluginState                 getState() const;
+       juce::AudioProcessorEditor* createEditor() const;
+
+       /* countMainOutChannels
+       Returns the current channel layout for the main output bus. */
+
+       int countMainOutChannels() const;
+
+       /* process
+       Process the plug-in with audio and MIDI data. The audio buffer is a 
+       reference, while the MIDI buffer must be passed by copy: each plug-in must 
+       receive its own copy of the event set, so that any attempt to change/clear 
+       the MIDI buffer will only modify the local copy. Returns a reference of the
+       local buffer filled with processed data. */
+
+       const Buffer& process(const Buffer& b, juce::MidiBuffer m);
+
+       void setState(PluginState p);
+       void setBypass(bool b);
+
+       /* id
+       Unique identifier. */
+
+       ID id;
+
+       /* midiInParams
+       A vector of MidiLearnParam's for controlling plug-in parameters with
+       external hardware. */
+
+       std::vector<MidiLearnParam> midiInParams;
+
+       /* valid
+       A missing plug-in is loaded anyway, yet marked as 'invalid'. */
+
+       bool valid;
+
+       std::function<void(int w, int h)> onEditorResize;
+
+private:
+#ifdef G_OS_WINDOWS
+/* Fuck... */
+#undef IN
+#undef OUT
+#endif
+
+       enum class BusType
+       {
+               IN  = true,
+               OUT = false
+       };
+
+       /* JUCE overrides. */
+
+       void componentMovedOrResized(juce::Component& c, bool moved, bool resized) override;
+
+       juce::AudioProcessor::Bus* getMainBus(BusType b) const;
+
+       std::unique_ptr<juce::AudioPluginInstance> m_plugin;
+       std::unique_ptr<PluginHost::Info>          m_playHead;
+       Buffer                                     m_buffer;
+
+       std::atomic<bool> m_bypass;
+
+       /* UID
+       The original UID, used for missing plugins. */
+
+       std::string m_UID;
+
+       /* m_hasEditor
+       Cached boolean value that tells if the plug-in has editor. Some plug-ins
+       take ages to query it, better fetch the property during construction. */
+
+       bool m_hasEditor;
+};
+} // namespace giada::m
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/core/plugins/pluginHost.cpp b/src/core/plugins/pluginHost.cpp
new file mode 100644 (file)
index 0000000..4d8c724
--- /dev/null
@@ -0,0 +1,234 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "core/plugins/pluginHost.h"
+#include "core/channels/channel.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "core/plugins/plugin.h"
+#include "core/plugins/pluginManager.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include "utils/log.h"
+#include "utils/vector.h"
+#include <cassert>
+#include <cstddef>
+#include <memory>
+
+namespace giada::m
+{
+PluginHost::Info::Info(const Sequencer& s, int sampleRate)
+: m_sequencer(s)
+, m_sampleRate(sampleRate)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool PluginHost::Info::getCurrentPosition(CurrentPositionInfo& result)
+{
+       result.bpm           = m_sequencer.getBpm();
+       result.timeInSamples = m_sequencer.getCurrentFrame();
+       result.timeInSeconds = m_sequencer.getCurrentSecond(m_sampleRate);
+       result.isPlaying     = m_sequencer.isRunning();
+
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool PluginHost::Info::canControlTransport()
+{
+       return false;
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+PluginHost::PluginHost(model::Model& m)
+: m_model(m)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::reset(int bufferSize)
+{
+       freeAllPlugins();
+       m_audioBuffer.setSize(G_MAX_IO_CHANS, bufferSize);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::processStack(mcl::AudioBuffer& outBuf, const std::vector<Plugin*>& plugins,
+    juce::MidiBuffer* events)
+{
+       assert(outBuf.countFrames() == m_audioBuffer.getNumSamples());
+
+       giadaToJuceTempBuf(outBuf);
+
+       if (events == nullptr)
+       {
+               juce::MidiBuffer dummyEvents; // empty
+               processPlugins(plugins, dummyEvents);
+       }
+       else
+               processPlugins(plugins, *events);
+
+       juceToGiadaOutBuf(outBuf);
+}
+
+/* -------------------------------------------------------------------------- */
+
+const Plugin& PluginHost::addPlugin(std::unique_ptr<Plugin> p)
+{
+       m_model.addShared(std::move(p));
+       return m_model.backShared<Plugin>();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::swapPlugin(const m::Plugin& p1, const m::Plugin& p2, std::vector<Plugin*>& plugins)
+{
+       std::size_t index1 = u::vector::indexOf(plugins, &p1);
+       std::size_t index2 = u::vector::indexOf(plugins, &p2);
+       std::swap(plugins.at(index1), plugins.at(index2));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::freePlugin(const m::Plugin& plugin)
+{
+       m_model.removeShared(plugin);
+}
+
+void PluginHost::freePlugins(const std::vector<Plugin*>& plugins)
+{
+       for (const Plugin* p : plugins)
+               m_model.removeShared(*p);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::freeAllPlugins()
+{
+       m_model.clearShared<model::PluginPtrs>();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::setPluginParameter(ID pluginId, int paramIndex, float value)
+{
+       m_model.findShared<Plugin>(pluginId)->setParameter(paramIndex, value);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::setPluginProgram(ID pluginId, int programIndex)
+{
+       m_model.findShared<Plugin>(pluginId)->setCurrentProgram(programIndex);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::toggleBypass(ID pluginId)
+{
+       Plugin& plugin = *m_model.findShared<Plugin>(pluginId);
+       plugin.setBypass(!plugin.isBypassed());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::giadaToJuceTempBuf(const mcl::AudioBuffer& outBuf)
+{
+       assert(outBuf.countChannels() == m_audioBuffer.getNumChannels());
+
+       using namespace juce;
+       using Format = AudioData::Format<AudioData::Float32, AudioData::BigEndian>;
+
+       AudioData::deinterleaveSamples(
+           AudioData::InterleavedSource<Format>{outBuf[0], outBuf.countChannels()},
+           AudioData::NonInterleavedDest<Format>{m_audioBuffer.getArrayOfWritePointers(), m_audioBuffer.getNumChannels()},
+           outBuf.countFrames());
+}
+
+void PluginHost::juceToGiadaOutBuf(mcl::AudioBuffer& outBuf) const
+{
+       assert(outBuf.countChannels() == m_audioBuffer.getNumChannels());
+
+       using namespace juce;
+       using Format = AudioData::Format<AudioData::Float32, AudioData::BigEndian>;
+
+       AudioData::interleaveSamples(
+           AudioData::NonInterleavedSource<Format>{m_audioBuffer.getArrayOfReadPointers(), m_audioBuffer.getNumChannels()},
+           AudioData::InterleavedDest<Format>{outBuf[0], outBuf.countChannels()},
+           outBuf.countFrames());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::processPlugins(const std::vector<Plugin*>& plugins, juce::MidiBuffer& events)
+{
+       for (Plugin* p : plugins)
+       {
+               if (!p->valid || p->isSuspended() || p->isBypassed())
+                       continue;
+               processPlugin(p, events);
+       }
+       events.clear();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginHost::processPlugin(Plugin* p, const juce::MidiBuffer& events)
+{
+       const Plugin::Buffer& pluginBuffer = p->process(m_audioBuffer, events);
+       const bool            isInstrument = p->isInstrument();
+
+       /* Merge the plugin buffer back into the local one. Special care is needed
+       if audio channels mismatch. */
+
+       for (int i = 0, j = 0; i < m_audioBuffer.getNumChannels(); i++)
+       {
+               /* If instrument (i.e. a plug-in that accepts MIDI and produces audio 
+               out of it), SUM the local working buffer to the main one. This allows
+               multiple plug-in instruments to play simultaneously on a given set of
+               MIDI events. If it's a normal FX instead (!isInstrument), the local
+               working buffer is simply copied over the main one. */
+
+               if (isInstrument)
+                       m_audioBuffer.addFrom(i, 0, pluginBuffer, j, 0, pluginBuffer.getNumSamples());
+               else
+                       m_audioBuffer.copyFrom(i, 0, pluginBuffer, j, 0, pluginBuffer.getNumSamples());
+               if (i < p->countMainOutChannels() - 1)
+                       j++;
+       }
+}
+} // namespace giada::m
+
+#endif // #ifdef WITH_VST
diff --git a/src/core/plugins/pluginHost.h b/src/core/plugins/pluginHost.h
new file mode 100644 (file)
index 0000000..2663690
--- /dev/null
@@ -0,0 +1,138 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef G_PLUGIN_HOST_H
+#define G_PLUGIN_HOST_H
+
+#include "core/types.h"
+#include "deps/juce-config.h"
+#include <functional>
+#include <memory>
+
+namespace mcl
+{
+class AudioBuffer;
+}
+
+namespace giada::m
+{
+class Plugin;
+}
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::m
+{
+class Sequencer;
+class PluginHost final
+{
+public:
+       class Info final : public juce::AudioPlayHead
+       {
+       public:
+               Info(const Sequencer&, int sampleRate);
+
+               bool getCurrentPosition(CurrentPositionInfo& result) override;
+               bool canControlTransport() override;
+
+       private:
+               const Sequencer& m_sequencer;
+               int              m_sampleRate;
+       };
+
+       PluginHost(model::Model&);
+
+       /* reset
+       Brings everything back to the initial state. */
+
+       void reset(int bufferSize);
+
+       /* addPlugin
+       Loads a new plugin into memory. Returns a reference to the newly created
+       object. */
+
+       const Plugin& addPlugin(std::unique_ptr<Plugin> p);
+
+       /* processStack
+       Applies the fx list to the buffer. */
+
+       void processStack(mcl::AudioBuffer& outBuf, const std::vector<Plugin*>& plugins,
+           juce::MidiBuffer* events = nullptr);
+
+       /* swapPlugin 
+       Swaps plug-in 1 with plug-in 2 in the plug-in vector. */
+
+       void swapPlugin(const m::Plugin& p1, const m::Plugin& p2, std::vector<Plugin*>& plugins);
+
+       /* freePlugin.
+       Unloads plugin from memory. */
+
+       void freePlugin(const m::Plugin& plugin);
+
+       /* freePlugins
+       Unloads multiple plugins. Useful when freeing or deleting a channel. */
+
+       void freePlugins(const std::vector<Plugin*>& plugins);
+
+       /* freeAllPlugins
+       Just deletes everything. */
+
+       void freeAllPlugins();
+
+       void setPluginParameter(ID pluginId, int paramIndex, float value);
+       void setPluginProgram(ID pluginId, int programIndex);
+       void toggleBypass(ID pluginId);
+
+private:
+       /* giadaToJuceTempBuf
+       Copies the Giada buffer 'outBuf' to the private JUCE buffer for local
+       processing. */
+
+       void giadaToJuceTempBuf(const mcl::AudioBuffer& outBuf);
+
+       /* juceToGiadaOutBuf
+       Copies the private JUCE buffer to Giada buffer 'outBuf'. */
+
+       void juceToGiadaOutBuf(mcl::AudioBuffer& outBuf) const;
+
+       void processPlugins(const std::vector<Plugin*>&, juce::MidiBuffer& events);
+
+       void processPlugin(Plugin*, const juce::MidiBuffer& events);
+
+       model::Model& m_model;
+
+       juce::AudioBuffer<float> m_audioBuffer;
+};
+} // namespace giada::m
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/core/plugins/pluginManager.cpp b/src/core/plugins/pluginManager.cpp
new file mode 100644 (file)
index 0000000..8ffbf6e
--- /dev/null
@@ -0,0 +1,310 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "core/plugins/pluginManager.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "core/patch.h"
+#include "core/plugins/plugin.h"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include <cassert>
+#include <cstddef>
+#include <memory>
+
+namespace giada::m
+{
+void PluginManager::reset(SortMethod sortMethod)
+{
+       m_pluginId       = IdManager();
+       m_missingPlugins = false;
+
+       m_unknownPluginList.clear();
+       if (m_formatManager.getNumFormats() == 0) // Must be called only once
+               m_formatManager.addDefaultFormats();
+
+       loadList(u::fs::getHomePath() + G_SLASH + "plugins.xml");
+       sortPlugins(sortMethod);
+}
+
+/* -------------------------------------------------------------------------- */
+
+int PluginManager::scanDirs(const std::string& dirs, const std::function<void(float)>& cb)
+{
+       u::log::print("[pluginManager::scanDir] requested directories: '%s'\n", dirs);
+       u::log::print("[pluginManager::scanDir] currently known plug-ins: %d\n", m_knownPluginList.getNumTypes());
+
+       m_knownPluginList.clear(); // clear up previous plugins
+
+       std::vector<std::string> dirVec = u::string::split(dirs, ";");
+
+       juce::FileSearchPath searchPath;
+       for (const std::string& dir : dirVec)
+               searchPath.add(juce::File(dir));
+
+       for (int i = 0; i < m_formatManager.getNumFormats(); i++)
+       {
+               juce::PluginDirectoryScanner scanner(m_knownPluginList, *m_formatManager.getFormat(i), searchPath,
+                   /*recursive=*/true, juce::File());
+
+               juce::String name;
+               while (scanner.scanNextFile(false, name))
+               {
+                       u::log::print("[pluginManager::scanDir]   scanning '%s'\n", name.toRawUTF8());
+                       cb(scanner.getProgress());
+               }
+       }
+
+       u::log::print("[pluginManager::scanDir] %d plugin(s) found\n", m_knownPluginList.getNumTypes());
+       return m_knownPluginList.getNumTypes();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool PluginManager::saveList(const std::string& filepath) const
+{
+       bool out = m_knownPluginList.createXml()->writeTo(juce::File(filepath));
+       if (!out)
+               u::log::print("[pluginManager::saveList] unable to save plugin list to %s\n", filepath);
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool PluginManager::loadList(const std::string& filepath)
+{
+       std::unique_ptr<juce::XmlElement> elem(juce::XmlDocument::parse(juce::File(filepath)));
+       if (elem == nullptr)
+               return false;
+       m_knownPluginList.recreateFromXml(*elem);
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unique_ptr<Plugin> PluginManager::makePlugin(const std::string& pid,
+    int sampleRate, int bufferSize, const Sequencer& sequencer, ID id)
+{
+       /* Plug-in ID generator is updated anyway, as we store Plugin objects also
+       if they are in an invalid state. */
+
+       m_pluginId.set(id);
+
+       const std::unique_ptr<juce::PluginDescription> pd = m_knownPluginList.getTypeForIdentifierString(pid);
+       if (pd == nullptr)
+       {
+               u::log::print("[pluginManager::makePlugin] no plugin found with pid=%s!\n", pid);
+               return makeInvalidPlugin(pid, id);
+       }
+
+       juce::String                               error;
+       std::unique_ptr<juce::AudioPluginInstance> pi = m_formatManager.createPluginInstance(*pd, sampleRate, bufferSize, error);
+       if (pi == nullptr)
+       {
+               u::log::print("[pluginManager::makePlugin] unable to create instance with pid=%s! Error: %s\n",
+                   pid, error.toStdString());
+               return makeInvalidPlugin(pid, id);
+       }
+
+       u::log::print("[pluginManager::makePlugin] plugin instance with pid=%s created\n", pid);
+
+       return std::make_unique<Plugin>(
+           m_pluginId.generate(id),
+           std::move(pi),
+           std::make_unique<PluginHost::Info>(sequencer, sampleRate),
+           sampleRate, bufferSize);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unique_ptr<Plugin> PluginManager::makePlugin(int index, int sampleRate,
+    int bufferSize, const Sequencer& sequencer)
+{
+       juce::PluginDescription pd = m_knownPluginList.getTypes()[index];
+
+       if (pd.uniqueId == 0) // Invalid
+               return {};
+
+       u::log::print("[pluginManager::makePlugin] plugin found, uid=%s, name=%s...\n",
+           pd.createIdentifierString().toRawUTF8(), pd.name.toRawUTF8());
+
+       return makePlugin(pd.createIdentifierString().toStdString(), sampleRate, bufferSize, sequencer);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unique_ptr<Plugin> PluginManager::makePlugin(const Plugin& src, int sampleRate,
+    int bufferSize, const Sequencer& sequencer)
+{
+       std::unique_ptr<Plugin> p = makePlugin(src.getUniqueId(), sampleRate, bufferSize, sequencer);
+
+       for (int i = 0; i < src.getNumParameters(); i++)
+               p->setParameter(i, src.getParameter(i));
+
+       return p;
+}
+
+/* -------------------------------------------------------------------------- */
+
+const Patch::Plugin PluginManager::serializePlugin(const Plugin& p) const
+{
+       Patch::Plugin pp;
+       pp.id     = p.id;
+       pp.path   = p.getUniqueId();
+       pp.bypass = p.isBypassed();
+       pp.state  = p.getState().asBase64();
+
+       for (const MidiLearnParam& param : p.midiInParams)
+               pp.midiInParams.push_back(param.getValue());
+
+       return pp;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unique_ptr<Plugin> PluginManager::deserializePlugin(const Patch::Plugin& p,
+    int sampleRate, int bufferSize, const Sequencer& sequencer)
+{
+       std::unique_ptr<Plugin> plugin = makePlugin(p.path, sampleRate, bufferSize, sequencer, p.id);
+       if (!plugin->valid)
+               return plugin; // Return invalid version
+
+       plugin->setBypass(p.bypass);
+       plugin->setState(PluginState(p.state));
+
+       /* Fill plug-in MidiIn parameters. Don't fill Plugin::midiInParam if 
+       Patch::midiInParams are zero: it would wipe out the current default 0x0
+       values. */
+
+       if (!p.midiInParams.empty())
+       {
+               plugin->midiInParams.clear();
+               std::size_t paramIndex = 0;
+               for (uint32_t midiInParam : p.midiInParams)
+                       plugin->midiInParams.emplace_back(midiInParam, paramIndex++);
+       }
+
+       return plugin;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<Plugin*> PluginManager::hydratePlugins(std::vector<ID> pluginIds, model::Model& model)
+{
+       std::vector<Plugin*> out;
+       for (ID id : pluginIds)
+       {
+               Plugin* plugin = model.findShared<Plugin>(id);
+               if (plugin != nullptr)
+                       out.push_back(plugin);
+       }
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int PluginManager::countAvailablePlugins() const
+{
+       return m_knownPluginList.getNumTypes();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<PluginManager::PluginInfo> PluginManager::getPluginsInfo() const
+{
+       std::vector<PluginInfo> out;
+
+       for (int i = 0; i < m_knownPluginList.getNumTypes(); i++)
+       {
+               juce::PluginDescription pd = m_knownPluginList.getTypes()[i];
+               PluginInfo              pi;
+
+               pi.uid              = pd.fileOrIdentifier.toStdString();
+               pi.name             = pd.descriptiveName.toStdString();
+               pi.category         = pd.category.toStdString();
+               pi.manufacturerName = pd.manufacturerName.toStdString();
+               pi.format           = pd.pluginFormatName.toStdString();
+               pi.isInstrument     = pd.isInstrument;
+               pi.exists           = m_formatManager.doesPluginStillExist(*m_knownPluginList.getTypeForFile(pi.uid));
+               pi.isKnown          = true;
+               out.push_back(pi);
+       }
+
+       for (const std::string& uid : m_unknownPluginList)
+       {
+               PluginInfo pi;
+               pi.uid          = uid;
+               pi.isInstrument = false;
+               pi.exists       = false;
+               pi.isKnown      = false;
+               out.push_back(pi);
+       }
+
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool PluginManager::hasMissingPlugins() const
+{
+       return m_missingPlugins;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void PluginManager::sortPlugins(SortMethod method)
+{
+       switch (method)
+       {
+       case SortMethod::NAME:
+               m_knownPluginList.sort(juce::KnownPluginList::SortMethod::sortAlphabetically, true);
+               break;
+       case SortMethod::CATEGORY:
+               m_knownPluginList.sort(juce::KnownPluginList::SortMethod::sortByCategory, true);
+               break;
+       case SortMethod::MANUFACTURER:
+               m_knownPluginList.sort(juce::KnownPluginList::SortMethod::sortByManufacturer, true);
+               break;
+       case SortMethod::FORMAT:
+               m_knownPluginList.sort(juce::KnownPluginList::SortMethod::sortByFormat, true);
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unique_ptr<Plugin> PluginManager::makeInvalidPlugin(const std::string& pid, ID id)
+{
+       m_missingPlugins = true;
+       m_unknownPluginList.push_back(pid);
+       return std::make_unique<Plugin>(m_pluginId.generate(id), pid); // Invalid plug-in
+}
+} // namespace giada::m
+
+#endif // #ifdef WITH_VST
diff --git a/src/core/plugins/pluginManager.h b/src/core/plugins/pluginManager.h
new file mode 100644 (file)
index 0000000..6e6b58a
--- /dev/null
@@ -0,0 +1,151 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef G_PLUGIN_MANAGER_H
+#define G_PLUGIN_MANAGER_H
+
+#include "core/idManager.h"
+#include "core/patch.h"
+#include "deps/juce-config.h"
+#include "plugin.h"
+#include <memory>
+
+namespace giada::m::patch
+{
+struct Plugin;
+struct Version;
+} // namespace giada::m::patch
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::m
+{
+class Sequencer;
+class PluginManager final
+{
+public:
+       enum class SortMethod : int
+       {
+               NAME = 0,
+               CATEGORY,
+               MANUFACTURER,
+               FORMAT
+       };
+
+       struct PluginInfo
+       {
+               std::string uid;
+               std::string name;
+               std::string category;
+               std::string manufacturerName;
+               std::string format;
+               bool        isInstrument;
+               bool        exists;
+               bool        isKnown;
+       };
+
+       /* getPluginsInfo
+       Returns a vector of PluginInfo objects containing all plug-ins, known and
+       unknown, scanned so far. */
+
+       std::vector<PluginInfo> getPluginsInfo() const;
+
+       /* hasMissingPlugins
+       True if some plug-ins have been marked as missing during the initial scan. */
+
+       bool hasMissingPlugins() const;
+
+       /* countAvailablePlugins
+       Returns how many plug-ins are ready and available for usage. */
+
+       int countAvailablePlugins() const;
+
+       /* reset
+       Brings everything back to the initial state. */
+
+       void reset(SortMethod);
+
+       /* scanDirs
+       Parses plugin directories (semicolon-separated) and store list in 
+       knownPluginList. The callback is called on each plugin found. Used to update
+       the main window from the GUI thread. */
+
+       int scanDirs(const std::string& paths, const std::function<void(float)>& cb);
+
+       /* (save|load)List
+       (Save|Load) knownPluginList (in|from) an XML file. */
+
+       bool saveList(const std::string& path) const;
+       bool loadList(const std::string& path);
+
+       std::unique_ptr<Plugin> makePlugin(const std::string& pid, int sampleRate, int bufferSize, const Sequencer&, ID id = 0);
+       std::unique_ptr<Plugin> makePlugin(int index, int sampleRate, int bufferSize, const Sequencer&);
+       std::unique_ptr<Plugin> makePlugin(const Plugin& other, int sampleRate, int bufferSize, const Sequencer&);
+
+       /* (de)serializePlugin
+       Transforms patch data into a Plugin object and vice versa. */
+
+       const Patch::Plugin     serializePlugin(const Plugin& p) const;
+       std::unique_ptr<Plugin> deserializePlugin(const Patch::Plugin&, int sampleRate, int bufferSize, const Sequencer&);
+       std::vector<Plugin*>    hydratePlugins(std::vector<ID> pluginIds, model::Model& model);
+
+       void sortPlugins(SortMethod sortMethod);
+
+private:
+       std::unique_ptr<Plugin> makeInvalidPlugin(const std::string& pid, ID id);
+
+       IdManager m_pluginId;
+
+       /* formatManager
+       Plugin format manager. */
+
+       juce::AudioPluginFormatManager m_formatManager;
+
+       /* knownPuginList
+       List of known (i.e. scanned) plugins. */
+
+       juce::KnownPluginList m_knownPluginList;
+
+       /* unknownPluginList
+       List of unrecognized plugins found in a patch. */
+
+       std::vector<std::string> m_unknownPluginList;
+
+       /* missingPlugins
+       If some plugins from any stack are missing. */
+
+       bool m_missingPlugins;
+};
+} // namespace giada::m
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/core/plugins/pluginState.cpp b/src/core/plugins/pluginState.cpp
new file mode 100644 (file)
index 0000000..17cb8f1
--- /dev/null
@@ -0,0 +1,69 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "pluginState.h"
+#include "core/const.h"
+#include <cstddef>
+
+namespace giada::m
+{
+PluginState::PluginState(juce::MemoryBlock&& data)
+: m_data(std::move(data))
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+PluginState::PluginState(const std::string& base64)
+{
+       bool res = m_data.fromBase64Encoding(base64);
+       if (!res)
+               G_DEBUG("Error while loading plug-in state!");
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string PluginState::asBase64() const
+{
+       return m_data.toBase64Encoding().toStdString();
+}
+
+/* -------------------------------------------------------------------------- */
+
+const void* PluginState::getData() const
+{
+       return m_data.getData();
+}
+
+size_t PluginState::getSize() const
+{
+       return m_data.getSize();
+}
+} // namespace giada::m
+
+#endif // #ifdef WITH_VST
diff --git a/src/core/plugins/pluginState.h b/src/core/plugins/pluginState.h
new file mode 100644 (file)
index 0000000..753e66d
--- /dev/null
@@ -0,0 +1,56 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef G_PLUGIN_STATE_H
+#define G_PLUGIN_STATE_H
+
+#include "deps/juce-config.h"
+#include <cstddef>
+#include <string>
+
+namespace giada::m
+{
+class PluginState
+{
+public:
+       PluginState() = default; // Invalid state
+       PluginState(juce::MemoryBlock&& data);
+       PluginState(const std::string& base64);
+
+       std::string asBase64() const;
+       const void* getData() const;
+       size_t      getSize() const;
+
+private:
+       juce::MemoryBlock m_data;
+};
+} // namespace giada::m
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/core/quantizer.cpp b/src/core/quantizer.cpp
new file mode 100644 (file)
index 0000000..224700e
--- /dev/null
@@ -0,0 +1,84 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "quantizer.h"
+#include <cassert>
+
+namespace giada::m
+{
+void Quantizer::trigger(int id)
+{
+       assert(m_callbacks.count(id) > 0); // Make sure id exists
+
+       m_performId.store(id);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Quantizer::schedule(int id, std::function<void(Frame delta)> f)
+{
+       m_callbacks[id] = f;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Quantizer::advance(Range<Frame> block, Frame quantizerStep)
+{
+       /* Nothing to do if there's no action to perform. */
+
+       const int pid = m_performId.load();
+
+       if (pid == -1)
+               return;
+
+       assert(m_callbacks.count(pid) > 0);
+
+       for (Frame global = block.getBegin(), local = 0; global < block.getEnd(); global++, local++)
+       {
+
+               if (global % quantizerStep != 0) // Skip if it's not on a quantization unit.
+                       continue;
+
+               m_callbacks.at(pid)(local);
+               m_performId.store(-1);
+               return;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Quantizer::clear()
+{
+       m_performId.store(-1);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Quantizer::hasBeenTriggered() const
+{
+       return m_performId.load() != -1;
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/quantizer.h b/src/core/quantizer.h
new file mode 100644 (file)
index 0000000..c2f9070
--- /dev/null
@@ -0,0 +1,77 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_QUANTIZER_H
+#define G_QUANTIZER_H
+
+#include "core/const.h"
+#include "core/range.h"
+#include "core/types.h"
+#include "core/weakAtomic.h"
+#include <functional>
+#include <map>
+
+namespace giada::m
+{
+class Quantizer
+{
+public:
+       /* schedule
+       Schedules a function in slot 'id' to be called at the right time. The 
+       function has a 'delta' parameter for the buffer offset. */
+
+       void schedule(int id, std::function<void(Frame)>);
+
+       /* trigger
+       Triggers the function in slot 'id'. Might start right away, or at the end 
+       of the quantization step. */
+
+       void trigger(int id);
+
+       /* advance
+       Computes the internal state. Wants a range of frames [currentFrame, 
+       currentFrame + bufferSize) and a quantization step. Call this function
+       on each block. */
+
+       void advance(Range<Frame> block, Frame quantizerStep);
+
+       /* clear
+       Disables quantized operations in progress, if any. */
+
+       void clear();
+
+       /* hasBeenTriggered
+       True if a quantizer function has been triggered(). */
+
+       bool hasBeenTriggered() const;
+
+private:
+       std::map<int, std::function<void(Frame)>> m_callbacks;
+       WeakAtomic<int>                           m_performId = -1;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/queue.h b/src/core/queue.h
new file mode 100644 (file)
index 0000000..811a50d
--- /dev/null
@@ -0,0 +1,90 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2019 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_QUEUE_H
+#define G_QUEUE_H
+
+#include <cstddef>
+#include <array>
+#include <atomic>
+
+namespace giada::m
+{
+/* Queue
+Single producer, single consumer lock-free queue. */
+
+template <typename T, std::size_t size>
+class Queue
+{
+public:
+       Queue()
+       : m_head(0)
+       , m_tail(0)
+       {
+       }
+
+       Queue(const Queue&) = delete;
+       Queue(Queue&&)      = delete;
+       Queue& operator=(const Queue&) = delete;
+       Queue& operator=(Queue&&) = delete;
+
+       bool pop(T& item)
+       {
+               std::size_t curr = m_head.load();
+               if (curr == m_tail.load()) // Queue empty, nothing to do
+                       return false;
+
+               item = m_data[curr];
+               m_head.store(increment(curr));
+               return true;
+       }
+
+       bool push(const T& item)
+       {
+               std::size_t curr = m_tail.load();
+               std::size_t next = increment(curr);
+
+               if (next == m_head.load()) // Queue full, nothing to do
+                       return false;
+
+               m_data[curr] = item;
+               m_tail.store(next);
+               return true;
+       }
+
+private:
+       std::size_t increment(std::size_t i) const
+       {
+               return (i + 1) % size;
+       }
+
+       std::array<T, size>      m_data;
+       std::atomic<std::size_t> m_head;
+       std::atomic<std::size_t> m_tail;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/range.h b/src/core/range.h
new file mode 100644 (file)
index 0000000..511dd1a
--- /dev/null
@@ -0,0 +1,65 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_RANGE_H
+#define G_RANGE_H
+
+#include <cassert>
+
+namespace giada
+{
+template <typename T>
+class Range
+{
+public:
+       Range()
+       : m_a(0)
+       , m_b(0)
+       {
+       }
+       Range(T a, T b)
+       : m_a(a)
+       , m_b(b)
+       {
+               assert(a < b);
+       }
+
+       T getBegin() const { return m_a; }
+       T getEnd() const { return m_b; }
+       T getLength() const { return m_b - m_a; }
+
+       bool contains(T t) const
+       {
+               return t >= m_a && t < m_b;
+       }
+
+  private:
+       T m_a;
+       T m_b;
+};
+} // namespace giada
+
+#endif
diff --git a/src/core/recorder.cpp b/src/core/recorder.cpp
new file mode 100644 (file)
index 0000000..19165cc
--- /dev/null
@@ -0,0 +1,227 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/recorder.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/sequencer.h"
+#include "core/types.h"
+#include "src/core/actions/actionRecorder.h"
+#include "src/core/actions/actions.h"
+
+namespace giada::m
+{
+Recorder::Recorder(model::Model& m, Sequencer& s, MixerHandler& mh)
+: m_model(m)
+, m_sequencer(s)
+, m_mixerHandler(mh)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Recorder::isRecording() const
+{
+       return isRecordingAction() || isRecordingInput();
+}
+
+bool Recorder::isRecordingAction() const
+{
+       return m_model.get().recorder.a_isRecordingAction();
+}
+
+bool Recorder::isRecordingInput() const
+{
+       return m_model.get().recorder.a_isRecordingInput();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Recorder::prepareActionRec(RecTriggerMode mode)
+{
+       if (mode == RecTriggerMode::NORMAL)
+       {
+               startActionRec();
+               m_sequencer.setStatus(SeqStatus::RUNNING);
+               G_DEBUG("Start action rec, NORMAL mode");
+       }
+       else
+       { // RecTriggerMode::SIGNAL
+               m_sequencer.setStatus(SeqStatus::WAITING);
+               G_DEBUG("Start action rec, SIGNAL mode (waiting for signal from Midi Dispatcher...)");
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Recorder::stopActionRec(ActionRecorder& actionRecorder)
+{
+       setRecordingAction(false);
+
+       /* If you stop the Action Recorder in SIGNAL mode before any actual 
+       recording: just clean up everything and return. */
+
+       if (m_sequencer.getStatus() == SeqStatus::WAITING)
+       {
+               m_sequencer.setStatus(SeqStatus::STOPPED);
+               return;
+       }
+
+       std::unordered_set<ID> channels = actionRecorder.consolidate();
+
+       /* Enable reading actions for Channels that have just been filled with 
+       actions. Start reading right away, without checking whether 
+       conf::treatRecsAsLoops is enabled or not. Same thing for MIDI channels.  */
+
+       for (ID id : channels)
+       {
+               Channel& ch = m_model.get().getChannel(id);
+               ch.shared->readActions.store(true);
+               ch.shared->recStatus.store(ChannelStatus::PLAY);
+               if (ch.type == ChannelType::MIDI)
+                       ch.shared->playStatus.store(ChannelStatus::PLAY);
+       }
+       m_model.swap(model::SwapType::HARD);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Recorder::prepareInputRec(RecTriggerMode triggerMode, InputRecMode inputMode)
+{
+       if (inputMode == InputRecMode::FREE)
+               m_sequencer.rewind();
+
+       if (triggerMode == RecTriggerMode::NORMAL)
+       {
+               startInputRec();
+               m_sequencer.setStatus(SeqStatus::RUNNING);
+               G_DEBUG("Start input rec, NORMAL mode");
+       }
+       else
+       {
+               m_sequencer.setStatus(SeqStatus::WAITING);
+               G_DEBUG("Start input rec, SIGNAL mode (waiting for signal from Mixer...)");
+       }
+
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Recorder::stopInputRec(InputRecMode recMode, int sampleRate)
+{
+       setRecordingInput(false);
+
+       Frame recordedFrames = m_mixerHandler.stopInputRec();
+
+       /* When recording in RIGID mode, the amount of recorded frames is always 
+       equal to the current loop length. */
+
+       if (recMode == InputRecMode::RIGID)
+               recordedFrames = m_sequencer.getFramesInLoop();
+
+       G_DEBUG("Stop input rec, recordedFrames=" << recordedFrames);
+
+       /* If you stop the Input Recorder in SIGNAL mode before any actual 
+       recording: just clean up everything and return. */
+
+       if (m_sequencer.getStatus() == SeqStatus::WAITING)
+       {
+               m_sequencer.setStatus(SeqStatus::STOPPED);
+               return;
+       }
+
+       /* Finalize recordings. InputRecMode::FREE requires some adjustments. */
+
+       m_mixerHandler.finalizeInputRec(recordedFrames, m_sequencer.getCurrentFrame());
+
+       if (recMode == InputRecMode::FREE)
+       {
+               m_sequencer.rewind();
+               m_sequencer.setBpm(m_sequencer.calcBpmFromRec(recordedFrames, sampleRate), sampleRate);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Recorder::canEnableRecOnSignal() const { return !m_sequencer.isRunning(); }
+bool Recorder::canEnableFreeInputRec() const { return !m_mixerHandler.hasAudioData(); }
+
+/* -------------------------------------------------------------------------- */
+
+bool Recorder::canRecordActions() const
+{
+       return isRecordingAction() && m_sequencer.isRunning() && !isRecordingInput();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Recorder::setRecordingAction(bool v)
+{
+       m_model.get().recorder.a_setRecordingAction(v);
+}
+
+void Recorder::setRecordingInput(bool v)
+{
+       m_model.get().recorder.a_setRecordingInput(v);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Recorder::startActionRec()
+{
+       setRecordingAction(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Recorder::startActionRecOnCallback()
+{
+       if (m_sequencer.getStatus() != SeqStatus::WAITING)
+               return;
+       startActionRec();
+       m_sequencer.setStatus(SeqStatus::RUNNING);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Recorder::startInputRec()
+{
+       /* Start recording from the current frame, not the beginning. */
+       m_mixerHandler.startInputRec(m_sequencer.getCurrentFrame());
+       setRecordingInput(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Recorder::startInputRecOnCallback()
+{
+       if (m_sequencer.getStatus() != SeqStatus::WAITING)
+               return;
+       startInputRec();
+       m_sequencer.setStatus(SeqStatus::RUNNING);
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/recorder.h b/src/core/recorder.h
new file mode 100644 (file)
index 0000000..34e4c4c
--- /dev/null
@@ -0,0 +1,88 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_REC_MANAGER_H
+#define G_REC_MANAGER_H
+
+#include "core/types.h"
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::m
+{
+class ActionRecorder;
+class MixerHandler;
+class Sequencer;
+class Recorder final
+{
+public:
+       Recorder(model::Model&, Sequencer&, MixerHandler&);
+
+       bool isRecording() const;
+       bool isRecordingAction() const;
+       bool isRecordingInput() const;
+
+       /* canEnableRecOnSignal
+    True if rec-on-signal can be enabled: can't set it while sequencer is 
+    running, in order to prevent mistakes while live recording. */
+
+       bool canEnableRecOnSignal() const;
+
+       /* canEnableFreeInputRec
+    True if free loop-length can be enabled: Can't set it if there's already a 
+    filled Sample Channel in the current project. */
+
+       bool canEnableFreeInputRec() const;
+
+       /* canRecordActions
+       True if actions are recordable right now. */
+
+       bool canRecordActions() const;
+
+       void prepareActionRec(RecTriggerMode);
+       void startActionRec();
+       void startActionRecOnCallback();
+       void stopActionRec(ActionRecorder&);
+
+       bool prepareInputRec(RecTriggerMode, InputRecMode);
+       void startInputRec();
+       void startInputRecOnCallback();
+       void stopInputRec(InputRecMode, int sampleRate);
+
+private:
+       void setRecordingAction(bool v);
+       void setRecordingInput(bool v);
+
+       model::Model& m_model;
+       Sequencer&    m_sequencer;
+       MixerHandler& m_mixerHandler;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/resampler.cpp b/src/core/resampler.cpp
new file mode 100644 (file)
index 0000000..4d22726
--- /dev/null
@@ -0,0 +1,176 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/resampler.h"
+#include <algorithm>
+#include <cassert>
+#include <new>
+#include <utility>
+
+#include <cstdio>
+
+namespace giada::m
+{
+Resampler::Resampler()
+: m_state(nullptr)
+, m_input(nullptr)
+, m_inputPos(0)
+, m_inputLength(0)
+, m_channels(0)
+, m_usedFrames(0)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+Resampler::Resampler(Quality quality, int channels)
+: Resampler()
+{
+       alloc(quality, channels);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Resampler::Resampler(const Resampler& o)
+: Resampler()
+{
+       *this = o;
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* This is a fake move constructor that makes a copy instead. The SRC_STATE
+object has a callback that, if moved, would still point to the original object.
+TODO: maybe delete the move constructor? */
+
+Resampler::Resampler(Resampler&& o)
+: Resampler()
+{
+       *this = o;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Resampler& Resampler::operator=(const Resampler& o)
+{
+       if (this == &o)
+               return *this;
+       alloc(o.m_quality, o.m_channels);
+       return *this;
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* This is a fake move operator: see notes above. */
+
+Resampler& Resampler::operator=(Resampler&& o)
+{
+       if (this == &o)
+               return *this;
+       alloc(o.m_quality, o.m_channels);
+       return *this;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Resampler::~Resampler()
+{
+       src_delete(m_state);
+}
+
+/* -------------------------------------------------------------------------- */
+
+long Resampler::callback(void* self, float** audio)
+{
+       return static_cast<Resampler*>(self)->callback(audio);
+}
+
+/* -------------------------------------------------------------------------- */
+
+long Resampler::callback(float** audio)
+{
+       assert(audio != nullptr);
+
+       /* Move pointer properly, taking into account read data and number of 
+       channels in input data. */
+
+       *audio = m_input + (m_inputPos * m_channels);
+
+       /* Returns how many frames have been read in this callback shot. */
+
+       long frames;
+
+       /* Read in CHUNK_LEN parts, checking if there is enough data left. */
+
+       if (m_inputPos + CHUNK_LEN < m_inputLength)
+               frames = CHUNK_LEN;
+       else
+               frames = m_inputLength - m_inputPos;
+
+       m_usedFrames += frames;
+       m_inputPos += frames;
+
+       return frames;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Resampler::alloc(Quality quality, int channels)
+{
+       if (m_state != nullptr)
+               src_delete(m_state);
+       m_state    = src_callback_new(callback, static_cast<int>(quality), channels, nullptr, this);
+       m_quality  = quality;
+       m_channels = channels;
+       if (m_state == nullptr)
+               throw std::bad_alloc();
+       src_reset(m_state);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Resampler::Result Resampler::process(float* input, long inputPos, long inputLength,
+    float* output, long outputLength, float ratio)
+{
+       assert(m_state != nullptr); // Must be initialized first!
+
+       m_input       = input;
+       m_inputPos    = inputPos;
+       m_inputLength = inputLength;
+       m_usedFrames  = 0;
+
+       long generated = src_callback_read(m_state, 1 / ratio, outputLength, output);
+
+       return {m_usedFrames, generated};
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Resampler::last()
+{
+       src_reset(m_state);
+}
+} // namespace giada::m
diff --git a/src/core/resampler.h b/src/core/resampler.h
new file mode 100644 (file)
index 0000000..2f563b6
--- /dev/null
@@ -0,0 +1,97 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_RESAMPLER_H
+#define G_RESAMPLER_H
+
+#include <cstddef>
+#include <samplerate.h>
+
+namespace giada::m
+{
+class Resampler final
+{
+public:
+       enum class Quality
+       {
+               SINC_BEST       = 0,
+               SINC_MEDIUM     = 1,
+               SINC_FASTEST    = 2,
+               ZERO_ORDER_HOLD = 3,
+               LINEAR          = 4
+       };
+
+       /* Result
+       A Result object is returned by the process() function below, containing the 
+       number of frames used from input and generated to output. */
+
+       struct Result
+       {
+               long used, generated;
+       };
+
+       Resampler(); // Invalid
+       Resampler(Quality quality, int channels);
+       Resampler(const Resampler& o);
+       Resampler(Resampler&&);
+       Resampler& operator=(const Resampler&);
+       Resampler& operator=(Resampler&&);
+       ~Resampler();
+
+       /* process
+       Resamples a certain amount of frames from 'input' starting at 'inputPos' and
+       puts the result into 'output'. */
+
+       Result process(float* input, long inputPos, long inputLength, float* output,
+           long outputLength, float ratio);
+
+       /* last
+       Call this when you are about to process the last chunk of data. */
+
+       void last();
+
+private:
+       static long callback(void* self, float** audio);
+       long        callback(float** audio);
+
+       void alloc(Quality quality, int channels);
+
+       /* CHUNK_LEN
+       How many chunks of data to read from input in the callback. */
+
+       static constexpr int CHUNK_LEN = 256;
+
+       SRC_STATE* m_state;
+       Quality    m_quality;
+       float*     m_input;       // Pointer to input data
+       long       m_inputPos;    // Where to read from input
+       long       m_inputLength; // Total number of frames in input data
+       int        m_channels;    // Number of channels
+       long       m_usedFrames;  // How many frames have been read from input with a process() call
+};
+} // namespace giada::m
+
+#endif
\ No newline at end of file
diff --git a/src/core/ringBuffer.h b/src/core/ringBuffer.h
new file mode 100644 (file)
index 0000000..04690a5
--- /dev/null
@@ -0,0 +1,79 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_RING_BUFFER_H
+#define G_RING_BUFFER_H
+
+#include <cstddef>
+#include <array>
+
+namespace giada
+{
+/* RingBuffer
+A non-thread-safe, fixed-size ring buffer implementation. It grows from 0 to S, 
+then items are overwritten starting from position 0. */
+
+template <typename T, std::size_t S>
+class RingBuffer
+{
+public:
+       using iterator       = typename std::array<T, S>::iterator;
+       using const_iterator = typename std::array<T, S>::const_iterator;
+
+       iterator       begin() { return m_data.begin(); }
+       iterator       end() { return m_data.begin() + m_end; }
+       const_iterator begin() const { return m_data.begin(); }
+       const_iterator end() const { return m_data.begin() + m_end; }
+       const_iterator cbegin() const { return m_data.begin(); }
+       const_iterator cend() const { return m_data.begin() + m_end; }
+
+       void clear()
+       {
+               m_data.fill({});
+               m_index = 0;
+               m_end   = 0;
+       }
+
+       void push_back(T t)
+       {
+               m_data[m_index] = t;
+               m_index         = (m_index + 1) % m_data.size(); // Wraps around at m_data.size()
+               m_end           = std::max(m_index, m_end);      // Points to the greater index
+       }
+
+       std::size_t size() const noexcept
+       {
+               return m_end;
+       }
+
+  private:
+       std::array<T, S> m_data;
+       std::size_t      m_index = 0;
+       std::size_t      m_end   = 0;
+};
+} // namespace giada
+
+#endif
diff --git a/src/core/sequencer.cpp b/src/core/sequencer.cpp
new file mode 100644 (file)
index 0000000..f479a51
--- /dev/null
@@ -0,0 +1,387 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/sequencer.h"
+#include "core/actions/actionRecorder.h"
+#include "core/jackTransport.h"
+#include "core/kernelAudio.h"
+#include "core/metronome.h"
+#include "core/model/model.h"
+#include "core/quantizer.h"
+#include "core/synchronizer.h"
+#include "utils/log.h"
+#include "utils/math.h"
+
+namespace giada::m
+{
+namespace
+{
+constexpr int Q_ACTION_REWIND = 0;
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Sequencer::Sequencer(model::Model& m, Synchronizer& s, JackTransport& j)
+: onAboutStart(nullptr)
+, onAboutStop(nullptr)
+, m_model(m)
+, m_synchronizer(s)
+, m_jackTransport(j)
+, m_quantizerStep(1)
+{
+       quantizer.schedule(Q_ACTION_REWIND, [this](Frame delta) { rewindQ(delta); });
+}
+/* -------------------------------------------------------------------------- */
+
+bool      Sequencer::canQuantize() const { return m_model.get().sequencer.canQuantize(); }
+bool      Sequencer::isRunning() const { return m_model.get().sequencer.isRunning(); }
+bool      Sequencer::isActive() const { return m_model.get().sequencer.isActive(); }
+bool      Sequencer::isOnBar() const { return m_model.get().sequencer.a_isOnBar(); }
+bool      Sequencer::isOnBeat() const { return m_model.get().sequencer.a_isOnBeat(); }
+bool      Sequencer::isOnFirstBeat() const { return m_model.get().sequencer.a_isOnFirstBeat(); }
+float     Sequencer::getBpm() const { return m_model.get().sequencer.bpm; }
+int       Sequencer::getBeats() const { return m_model.get().sequencer.beats; }
+int       Sequencer::getBars() const { return m_model.get().sequencer.bars; }
+int       Sequencer::getCurrentBeat() const { return m_model.get().sequencer.a_getCurrentBeat(); }
+Frame     Sequencer::getCurrentFrame() const { return m_model.get().sequencer.a_getCurrentFrame(); }
+Frame     Sequencer::getCurrentFrameQuantized() const { return quantize(getCurrentFrame()); }
+float     Sequencer::getCurrentSecond(int sampleRate) const { return getCurrentFrame() / static_cast<float>(sampleRate); }
+Frame     Sequencer::getFramesInBar() const { return m_model.get().sequencer.framesInBar; }
+Frame     Sequencer::getFramesInBeat() const { return m_model.get().sequencer.framesInBeat; }
+Frame     Sequencer::getFramesInLoop() const { return m_model.get().sequencer.framesInLoop; }
+Frame     Sequencer::getFramesInSeq() const { return m_model.get().sequencer.framesInSeq; }
+int       Sequencer::getQuantizerValue() const { return m_model.get().sequencer.quantize; }
+int       Sequencer::getQuantizerStep() const { return m_quantizerStep; }
+SeqStatus Sequencer::getStatus() const { return m_model.get().sequencer.status; }
+
+/* -------------------------------------------------------------------------- */
+
+Frame Sequencer::getMaxFramesInLoop(int sampleRate) const
+{
+       return (sampleRate * (60.0f / G_MIN_BPM)) * getBeats();
+}
+
+/* -------------------------------------------------------------------------- */
+
+float Sequencer::calcBpmFromRec(Frame recordedFrames, int sampleRate) const
+{
+       return (60.0f * getBeats()) / (recordedFrames / static_cast<float>(sampleRate));
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame Sequencer::quantize(Frame f) const
+{
+       if (!canQuantize())
+               return f;
+       return u::math::quantize(f, m_quantizerStep) % getFramesInLoop(); // No overflow
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::reset(int sampleRate)
+{
+       model::Sequencer& s = m_model.get().sequencer;
+
+       s.bars     = G_DEFAULT_BARS;
+       s.beats    = G_DEFAULT_BEATS;
+       s.bpm      = G_DEFAULT_BPM;
+       s.quantize = G_DEFAULT_QUANTIZE;
+       recomputeFrames(sampleRate); // Model swap is done here, no need to call it twice
+       rewind();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::react(const EventDispatcher::EventBuffer& events, int sampleRate)
+{
+       for (const EventDispatcher::Event& e : events)
+       {
+               switch (e.type)
+               {
+               case EventDispatcher::EventType::SEQUENCER_START:
+                       if (!m_jackTransport.start())
+                               rawStart();
+                       break;
+
+               case EventDispatcher::EventType::SEQUENCER_STOP:
+                       if (!m_jackTransport.stop())
+                               rawStop();
+                       break;
+
+               case EventDispatcher::EventType::SEQUENCER_REWIND:
+                       if (!m_jackTransport.setPosition(0))
+                               rawRewind();
+                       break;
+
+#ifdef WITH_AUDIO_JACK
+               case EventDispatcher::EventType::SEQUENCER_START_JACK:
+                       rawStart();
+                       break;
+
+               case EventDispatcher::EventType::SEQUENCER_STOP_JACK:
+                       rawStop();
+                       break;
+
+               case EventDispatcher::EventType::SEQUENCER_REWIND_JACK:
+                       rawRewind();
+                       break;
+
+               case EventDispatcher::EventType::SEQUENCER_BPM_JACK:
+                       rawSetBpm(std::get<float>(e.data), sampleRate);
+                       break;
+#endif
+
+               default:
+                       break;
+               }
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+const Sequencer::EventBuffer& Sequencer::advance(Frame bufferSize, const ActionRecorder& actionRecorder)
+{
+       m_eventBuffer.clear();
+
+       const model::Sequencer& sequencer = m_model.get().sequencer;
+
+       const Frame start        = sequencer.a_getCurrentFrame();
+       const Frame end          = start + bufferSize;
+       const Frame framesInLoop = sequencer.framesInLoop;
+       const Frame framesInBar  = sequencer.framesInBar;
+       const Frame framesInBeat = sequencer.framesInBeat;
+       const Frame nextFrame    = end % framesInLoop;
+       const int   nextBeat     = nextFrame / framesInBeat;
+
+       /* Process events in the current block. */
+
+       for (Frame i = start, local = 0; i < end; i++, local++)
+       {
+
+               Frame global = i % framesInLoop; // wraps around 'framesInLoop'
+
+               if (global == 0)
+               {
+                       m_eventBuffer.push_back({EventType::FIRST_BEAT, global, local});
+                       m_metronome.trigger(Metronome::Click::BEAT, local);
+               }
+               else if (global % framesInBar == 0)
+               {
+                       m_eventBuffer.push_back({EventType::BAR, global, local});
+                       m_metronome.trigger(Metronome::Click::BAR, local);
+               }
+               else if (global % framesInBeat == 0)
+               {
+                       m_metronome.trigger(Metronome::Click::BEAT, local);
+               }
+
+               const std::vector<Action>* as = actionRecorder.getActionsOnFrame(global);
+               if (as != nullptr)
+                       m_eventBuffer.push_back({EventType::ACTIONS, global, local, as});
+       }
+
+       /* Advance this and quantizer after the event parsing. */
+
+       sequencer.a_setCurrentFrame(nextFrame);
+       sequencer.a_setCurrentBeat(nextBeat);
+       quantizer.advance(Range<Frame>(start, end), getQuantizerStep());
+
+       return m_eventBuffer;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::render(mcl::AudioBuffer& outBuf)
+{
+       if (m_metronome.running)
+               m_metronome.render(outBuf);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::rawStart()
+{
+       assert(onAboutStart != nullptr);
+
+       const SeqStatus status = getStatus();
+
+       onAboutStart(status);
+
+       switch (status)
+       {
+       case SeqStatus::STOPPED:
+               setStatus(SeqStatus::RUNNING);
+               break;
+       case SeqStatus::WAITING:
+               setStatus(SeqStatus::RUNNING);
+               break;
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::rawStop()
+{
+       assert(onAboutStop != nullptr);
+
+       onAboutStop();
+       setStatus(SeqStatus::STOPPED);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::rawRewind()
+{
+       if (canQuantize())
+               quantizer.trigger(Q_ACTION_REWIND);
+       else
+               rewindQ(/*delta=*/0);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::rewind()
+{
+       const model::Sequencer& c = m_model.get().sequencer;
+
+       c.a_setCurrentFrame(0);
+       c.a_setCurrentBeat(0);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Sequencer::isMetronomeOn() const { return m_metronome.running; }
+void Sequencer::toggleMetronome() { m_metronome.running = !m_metronome.running; }
+void Sequencer::setMetronome(bool v) { m_metronome.running = v; }
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::rewindQ(Frame delta)
+{
+       rewind();
+       m_eventBuffer.push_back({EventType::REWIND, 0, delta});
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::recomputeFrames(int sampleRate)
+{
+       model::Sequencer& s = m_model.get().sequencer;
+
+       s.framesInLoop = static_cast<int>((sampleRate * (60.0f / s.bpm)) * s.beats);
+       s.framesInBar  = static_cast<int>(s.framesInLoop / (float)s.bars);
+       s.framesInBeat = static_cast<int>(s.framesInLoop / (float)s.beats);
+       s.framesInSeq  = s.framesInBeat * G_MAX_BEATS;
+
+       if (s.quantize != 0)
+               m_quantizerStep = s.framesInBeat / s.quantize;
+
+       m_model.swap(model::SwapType::NONE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::setBpm(float b, int sampleRate)
+{
+       b = std::clamp(b, G_MIN_BPM, G_MAX_BPM);
+
+       /* If JACK is being used, let it handle the bpm change. */
+       if (!m_jackTransport.setBpm(b))
+               rawSetBpm(b, sampleRate);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::rawSetBpm(float v, int sampleRate)
+{
+       assert(onBpmChange != nullptr);
+
+       const float oldVal = m_model.get().sequencer.bpm;
+       const float newVal = v;
+
+       m_model.get().sequencer.bpm = newVal;
+       recomputeFrames(sampleRate);
+       m_model.swap(model::SwapType::HARD);
+
+       onBpmChange(oldVal, newVal, m_quantizerStep);
+
+       u::log::print("[clock::rawSetBpm] Bpm changed to %f\n", newVal);
+}
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::setBeats(int newBeats, int newBars, int sampleRate)
+{
+       newBeats = std::clamp(newBeats, 1, G_MAX_BEATS);
+       newBars  = std::clamp(newBars, 1, newBeats); // Bars cannot be greater than beats
+
+       m_model.get().sequencer.beats = newBeats;
+       m_model.get().sequencer.bars  = newBars;
+       recomputeFrames(sampleRate);
+
+       m_model.swap(model::SwapType::HARD);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::setQuantize(int q, int sampleRate)
+{
+       m_model.get().sequencer.quantize = q;
+       recomputeFrames(sampleRate);
+
+       m_model.swap(model::SwapType::HARD);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Sequencer::setStatus(SeqStatus s)
+{
+       m_model.get().sequencer.status = s;
+       m_model.swap(model::SwapType::SOFT);
+
+       /* Additional things to do when the status changes. */
+
+       switch (s)
+       {
+       case SeqStatus::WAITING:
+               rewind();
+               m_synchronizer.sendMIDIrewind();
+               break;
+       case SeqStatus::STOPPED:
+               m_synchronizer.sendMIDIstop();
+               break;
+       case SeqStatus::RUNNING:
+               m_synchronizer.sendMIDIstart();
+               break;
+       default:
+               break;
+       }
+}
+} // namespace giada::m
\ No newline at end of file
diff --git a/src/core/sequencer.h b/src/core/sequencer.h
new file mode 100644 (file)
index 0000000..97f53d2
--- /dev/null
@@ -0,0 +1,213 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_SEQUENCER_H
+#define G_SEQUENCER_H
+
+#include "core/eventDispatcher.h"
+#include "core/metronome.h"
+#include "core/quantizer.h"
+#include <vector>
+
+namespace mcl
+{
+class AudioBuffer;
+}
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::m
+{
+class JackTransport;
+class ActionRecorder;
+class Synchronizer;
+class Sequencer final
+{
+public:
+       enum class EventType
+       {
+               NONE,
+               FIRST_BEAT,
+               BAR,
+               REWIND,
+               ACTIONS
+       };
+
+       struct Event
+       {
+               EventType                  type    = EventType::NONE;
+               Frame                      global  = 0;
+               Frame                      delta   = 0;
+               const std::vector<Action>* actions = nullptr;
+       };
+
+       using EventBuffer = RingBuffer<Event, G_MAX_SEQUENCER_EVENTS>;
+
+       Sequencer(model::Model&, Synchronizer&, JackTransport&);
+
+       /* canQuantize
+    Tells whether the quantizer value is > 0 and the sequencer is running. */
+
+       bool canQuantize() const;
+
+       /* isRunning
+    When sequencer is actually moving forward, i.e. SeqStatus == RUNNING. */
+
+       bool isRunning() const;
+
+       /* isActive
+    Sequencer is enabled, but might be in wait mode, i.e. SeqStatus == RUNNING or
+    SeqStatus == WAITING. */
+
+       bool isActive() const;
+
+       bool isOnBeat() const;
+       bool isOnBar() const;
+       bool isOnFirstBeat() const;
+       bool isMetronomeOn() const;
+
+       float     getBpm() const;
+       int       getBeats() const;
+       int       getBars() const;
+       int       getCurrentBeat() const;
+       Frame     getCurrentFrame() const;
+       Frame     getCurrentFrameQuantized() const;
+       float     getCurrentSecond(int sampleRate) const;
+       Frame     getFramesInBar() const;
+       Frame     getFramesInBeat() const;
+       Frame     getFramesInLoop() const;
+       Frame     getFramesInSeq() const;
+       int       getQuantizerValue() const;
+       int       getQuantizerStep() const;
+       SeqStatus getStatus() const;
+
+       /* getMaxFramesInLoop
+    Returns how many frames the current loop length might contain at the slowest
+    speed possible (G_MIN_BPM). Call this whenever you change the number or 
+    beats. */
+
+       Frame getMaxFramesInLoop(int sampleRate) const;
+
+       /* calcBpmFromRec
+    Given the amount of recorded frames, returns the speed of the current 
+    performance. Used while input recording in FREE mode. */
+
+       float calcBpmFromRec(Frame recordedFrames, int sampleRate) const;
+
+       /* quantize
+    Quantizes the frame 'f'.  */
+
+       Frame quantize(Frame f) const;
+
+       /* reset
+       Brings everything back to the initial state. */
+
+       void reset(int sampleRate);
+
+       /* react
+       Reacts to live events coming from the EventDispatcher (human events). */
+
+       void react(const EventDispatcher::EventBuffer&, int sampleRate);
+
+       /* advance
+       Parses sequencer events that might occur in a block and advances the internal 
+       quantizer. Returns a reference to the internal EventBuffer filled with events
+       (if any). Call this on each new audio block. */
+
+       const EventBuffer& advance(Frame bufferSize, const ActionRecorder&);
+
+       /* render
+       Renders audio coming out from the sequencer: that is, the metronome! */
+
+       void render(mcl::AudioBuffer& outBuf);
+
+       void rewind();
+       void toggleMetronome();
+       void setMetronome(bool v);
+       void setBpm(float b, int sampleRate);
+       void setBeats(int beats, int bars, int sampleRate);
+       void setQuantize(int q, int sampleRate);
+       void setStatus(SeqStatus);
+
+       /* recomputeFrames
+    Updates bpm, frames, beats and so on. */
+
+       void recomputeFrames(int sampleRate);
+
+       /* quantizer
+       Used by the sequencer itself and each sample channel. */
+
+       Quantizer quantizer;
+
+       std::function<void(SeqStatus)>         onAboutStart;
+       std::function<void()>                  onAboutStop;
+       std::function<void(float, float, int)> onBpmChange;
+
+private:
+       /* rewindQ
+       Rewinds sequencer, quantized mode. */
+
+       void rewindQ(Frame delta);
+
+       /* raw[*]
+       Raw functions to start, stop and rewind the sequencer. These functions must 
+       be called only when the JACK signal is received. Other modules should send
+       a SEQUENCER_* event to the Event Dispatcher. */
+
+       void rawStart();
+       void rawStop();
+       void rawRewind();
+
+       /* rawSetBpm
+       Raw function to set the bpm, bypassing any JACK instruction. This function 
+       must be called only by the Synchronizer when the JACK signal is received. 
+       Other modules should use the public, non-raw version setBpm(...). */
+
+       void rawSetBpm(float v, int sampleRate);
+
+       model::Model&  m_model;
+       Synchronizer&  m_synchronizer;
+       JackTransport& m_jackTransport;
+
+       /* m_eventBuffer
+       Buffer of events found in each block sent to channels for event parsing. 
+       This is filled during react(). */
+
+       EventBuffer m_eventBuffer;
+
+       Metronome m_metronome;
+
+       /* m_quantizerStep
+    Tells how many frames to wait to perform a quantized action. */
+
+       int m_quantizerStep;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/synchronizer.cpp b/src/core/synchronizer.cpp
new file mode 100644 (file)
index 0000000..504806f
--- /dev/null
@@ -0,0 +1,221 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/synchronizer.h"
+#include "core/conf.h"
+#include "core/kernelAudio.h"
+#include "core/kernelMidi.h"
+#include "core/model/model.h"
+
+namespace giada::m
+{
+Synchronizer::Synchronizer(const Conf::Data& c, KernelMidi& k)
+#ifdef WITH_AUDIO_JACK
+: onJackRewind(nullptr)
+, onJackChangeBpm(nullptr)
+, onJackStart(nullptr)
+, onJackStop(nullptr)
+, m_kernelMidi(k)
+, m_conf(c)
+#else
+: m_kernelMidi(k)
+, m_conf(c)
+#endif
+{
+       reset();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Synchronizer::reset()
+{
+       m_midiTCrate = static_cast<int>((m_conf.samplerate / m_conf.midiTCfps) * G_MAX_IO_CHANS); // stereo values
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Synchronizer::sendMIDIsync(const model::Sequencer& sequencer)
+{
+       /* Sending MIDI sync while waiting is meaningless. */
+
+       if (sequencer.status == SeqStatus::WAITING)
+               return;
+
+       const int currentFrame = sequencer.a_getCurrentFrame();
+
+       /* TODO - only Master (_M) is implemented so far. */
+
+       if (m_conf.midiSync == G_MIDI_SYNC_CLOCK_M)
+       {
+               if (currentFrame % (sequencer.framesInBeat / 24) == 0)
+                       m_kernelMidi.send(MIDI_CLOCK, -1, -1);
+               return;
+       }
+
+       if (m_conf.midiSync == G_MIDI_SYNC_MTC_M)
+       {
+
+               /* check if a new timecode frame has passed. If so, send MIDI TC
+                * quarter frames. 8 quarter frames, divided in two branches:
+                * 1-4 and 5-8. We check timecode frame's parity: if even, send
+                * range 1-4, if odd send 5-8. */
+
+               if (currentFrame % m_midiTCrate != 0) // no timecode frame passed
+                       return;
+
+               /* frame low nibble
+                * frame high nibble
+                * seconds low nibble
+                * seconds high nibble */
+
+               if (m_midiTCframes % 2 == 0)
+               {
+                       m_kernelMidi.send(MIDI_MTC_QUARTER, (m_midiTCframes & 0x0F) | 0x00, -1);
+                       m_kernelMidi.send(MIDI_MTC_QUARTER, (m_midiTCframes >> 4) | 0x10, -1);
+                       m_kernelMidi.send(MIDI_MTC_QUARTER, (m_midiTCseconds & 0x0F) | 0x20, -1);
+                       m_kernelMidi.send(MIDI_MTC_QUARTER, (m_midiTCseconds >> 4) | 0x30, -1);
+               }
+
+               /* minutes low nibble
+                * minutes high nibble
+                * hours low nibble
+                * hours high nibble SMPTE frame rate */
+
+               else
+               {
+                       m_kernelMidi.send(MIDI_MTC_QUARTER, (m_midiTCminutes & 0x0F) | 0x40, -1);
+                       m_kernelMidi.send(MIDI_MTC_QUARTER, (m_midiTCminutes >> 4) | 0x50, -1);
+                       m_kernelMidi.send(MIDI_MTC_QUARTER, (m_midiTChours & 0x0F) | 0x60, -1);
+                       m_kernelMidi.send(MIDI_MTC_QUARTER, (m_midiTChours >> 4) | 0x70, -1);
+               }
+
+               m_midiTCframes++;
+
+               /* check if total timecode frames are greater than timecode fps:
+                * if so, a second has passed */
+
+               if (m_midiTCframes > m_conf.midiTCfps)
+               {
+                       m_midiTCframes = 0;
+                       m_midiTCseconds++;
+                       if (m_midiTCseconds >= 60)
+                       {
+                               m_midiTCminutes++;
+                               m_midiTCseconds = 0;
+                               if (m_midiTCminutes >= 60)
+                               {
+                                       m_midiTChours++;
+                                       m_midiTCminutes = 0;
+                               }
+                       }
+                       //u::log::print("%d:%d:%d:%d\n", m_midiTChours, m_midiTCminutes, m_midiTCseconds, m_midiTCframes);
+               }
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Synchronizer::sendMIDIrewind()
+{
+       m_midiTCframes  = 0;
+       m_midiTCseconds = 0;
+       m_midiTCminutes = 0;
+       m_midiTChours   = 0;
+
+       /* For cueing the slave to a particular start point, Quarter Frame messages 
+    are not used. Instead, an MTC Full Frame message should be sent. The Full 
+    Frame is a SysEx message that encodes the entire SMPTE time in one message. */
+
+       if (m_conf.midiSync == G_MIDI_SYNC_MTC_M)
+       {
+               m_kernelMidi.send(MIDI_SYSEX, 0x7F, 0x00); // send msg on channel 0
+               m_kernelMidi.send(0x01, 0x01, 0x00);       // hours 0
+               m_kernelMidi.send(0x00, 0x00, 0x00);       // mins, secs, frames 0
+               m_kernelMidi.send(MIDI_EOX, -1, -1);       // end of sysex
+       }
+       else if (m_conf.midiSync == G_MIDI_SYNC_CLOCK_M)
+               m_kernelMidi.send(MIDI_POSITION_PTR, 0, 0);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Synchronizer::sendMIDIstart()
+{
+       if (m_conf.midiSync == G_MIDI_SYNC_CLOCK_M)
+       {
+               m_kernelMidi.send(MIDI_START, -1, -1);
+               m_kernelMidi.send(MIDI_POSITION_PTR, 0, 0);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Synchronizer::sendMIDIstop()
+{
+       if (m_conf.midiSync == G_MIDI_SYNC_CLOCK_M)
+               m_kernelMidi.send(MIDI_STOP, -1, -1);
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_AUDIO_JACK
+
+void Synchronizer::recvJackSync(const JackTransport::State& state)
+{
+       assert(onJackRewind != nullptr);
+       assert(onJackChangeBpm != nullptr);
+       assert(onJackStart != nullptr);
+       assert(onJackStop != nullptr);
+
+       JackTransport::State jackStateCurr = state;
+
+       if (jackStateCurr != m_jackStatePrev)
+       {
+               if (jackStateCurr.frame != m_jackStatePrev.frame && jackStateCurr.frame == 0)
+               {
+                       G_DEBUG("JackState received - rewind to frame 0");
+                       onJackRewind();
+               }
+
+               // jackStateCurr.bpm == 0 if JACK doesn't send that info
+               if (jackStateCurr.bpm != m_jackStatePrev.bpm && jackStateCurr.bpm > 1.0f)
+               {
+                       G_DEBUG("JackState received - bpm=" << jackStateCurr.bpm);
+                       onJackChangeBpm(jackStateCurr.bpm);
+               }
+
+               if (jackStateCurr.running != m_jackStatePrev.running)
+               {
+                       G_DEBUG("JackState received - running=" << jackStateCurr.running);
+                       jackStateCurr.running ? onJackStart() : onJackStop();
+               }
+       }
+
+       m_jackStatePrev = jackStateCurr;
+}
+
+#endif
+} // namespace giada::m
diff --git a/src/core/synchronizer.h b/src/core/synchronizer.h
new file mode 100644 (file)
index 0000000..2251847
--- /dev/null
@@ -0,0 +1,110 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_SYNC_H
+#define G_SYNC_H
+
+#ifdef WITH_AUDIO_JACK
+#include "core/jackTransport.h"
+#endif
+#include "core/types.h"
+#include "core/conf.h"
+#include <functional>
+
+namespace giada::m::kernelAudio
+{
+struct JackState;
+}
+
+namespace giada::m::model
+{
+class Sequencer;
+}
+
+namespace giada::m
+{
+class KernelMidi;
+class Synchronizer final
+{
+public:
+       Synchronizer(const Conf::Data&, KernelMidi&);
+
+       /* reset
+       Brings everything back to the initial state. */
+
+       void reset();
+
+       /* sendMIDIsync
+    Generates MIDI sync output data. */
+
+       void sendMIDIsync(const model::Sequencer& clock);
+
+       /* sendMIDIrewind
+    Rewinds timecode to beat 0 and also send a MTC full frame to cue the slave. */
+
+       void sendMIDIrewind();
+
+       void sendMIDIstart();
+       void sendMIDIstop();
+
+#ifdef WITH_AUDIO_JACK
+
+       /* recvJackSync
+    Receives a new JACK state. Called by Kernel Audio on each audio block. */
+
+       void recvJackSync(const JackTransport::State& state);
+
+       /* onJack[...]
+    Callbacks called when something happens in the JACK state. */
+
+       std::function<void()>      onJackRewind;
+       std::function<void(float)> onJackChangeBpm;
+       std::function<void()>      onJackStart;
+       std::function<void()>      onJackStop;
+#endif
+
+private:
+       /* midiTC*
+    MIDI timecode variables. */
+
+       int m_midiTCrate    = 0; // Send MTC data every m_midiTCrate frames
+       int m_midiTCframes  = 0;
+       int m_midiTCseconds = 0;
+       int m_midiTCminutes = 0;
+       int m_midiTChours   = 0;
+
+#ifdef WITH_AUDIO_JACK
+
+       JackTransport::State m_jackStatePrev;
+
+#endif
+
+       KernelMidi&       m_kernelMidi;
+       const Conf::Data& m_conf;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/types.h b/src/core/types.h
new file mode 100644 (file)
index 0000000..f19b6a4
--- /dev/null
@@ -0,0 +1,119 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_TYPES_H
+#define G_TYPES_H
+
+namespace giada
+{
+using ID    = int;
+using Pixel = int;
+using Frame = int;
+
+enum class Thread
+{
+       MAIN,
+       MIDI,
+       AUDIO,
+       EVENTS
+};
+
+/* Windows fix */
+#ifdef _WIN32
+#undef VOID
+#endif
+enum class SeqStatus
+{
+       STOPPED,
+       WAITING,
+       RUNNING,
+       ON_BEAT,
+       ON_BAR,
+       ON_FIRST_BEAT,
+       VOID
+};
+
+enum class ChannelType : int
+{
+       SAMPLE = 1,
+       MIDI,
+       MASTER,
+       PREVIEW
+};
+
+enum class ChannelStatus : int
+{
+       ENDING = 1,
+       WAIT,
+       PLAY,
+       OFF,
+       EMPTY,
+       MISSING,
+       WRONG
+};
+
+enum class SamplePlayerMode : int
+{
+       LOOP_BASIC = 1,
+       LOOP_ONCE,
+       LOOP_REPEAT,
+       LOOP_ONCE_BAR,
+       SINGLE_BASIC,
+       SINGLE_PRESS,
+       SINGLE_RETRIG,
+       SINGLE_ENDLESS,
+       SINGLE_BASIC_PAUSE
+};
+
+enum class RecTriggerMode : int
+{
+       NORMAL = 0,
+       SIGNAL
+};
+
+enum class InputRecMode : int
+{
+       RIGID = 0,
+       FREE
+};
+
+enum class EventType : int
+{
+       AUTO = 0,
+       MANUAL
+};
+
+/* Peak
+Audio peak information for two In/Out channels. */
+
+struct Peak
+{
+       float left;
+       float right;
+};
+} // namespace giada
+
+#endif
diff --git a/src/core/wave.cpp b/src/core/wave.cpp
new file mode 100644 (file)
index 0000000..264f1d7
--- /dev/null
@@ -0,0 +1,124 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "wave.h"
+#include "const.h"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include <cassert>
+
+namespace giada::m
+{
+Wave::Wave(ID id)
+: id(id)
+, m_rate(0)
+, m_bits(0)
+, m_logical(false)
+, m_edited(false)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+Wave::Wave(const Wave& other)
+: id(other.id)
+, m_buffer(other.getBuffer())
+, m_rate(other.m_rate)
+, m_bits(other.m_bits)
+, m_logical(false)
+, m_edited(false)
+, m_path(other.m_path)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Wave::alloc(Frame size, int channels, int rate, int bits, const std::string& path)
+{
+       m_buffer.alloc(size, channels);
+       m_rate = rate;
+       m_bits = bits;
+       m_path = path;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string Wave::getBasename(bool ext) const
+{
+       return ext ? u::fs::basename(m_path) : u::fs::stripExt(u::fs::basename(m_path));
+}
+
+/* -------------------------------------------------------------------------- */
+
+int         Wave::getRate() const { return m_rate; }
+std::string Wave::getPath() const { return m_path; }
+int         Wave::getBits() const { return m_bits; }
+bool        Wave::isLogical() const { return m_logical; }
+bool        Wave::isEdited() const { return m_edited; }
+
+/* -------------------------------------------------------------------------- */
+
+mcl::AudioBuffer&       Wave::getBuffer() { return m_buffer; }
+const mcl::AudioBuffer& Wave::getBuffer() const { return m_buffer; }
+
+/* -------------------------------------------------------------------------- */
+
+int Wave::getDuration() const
+{
+       return m_buffer.countFrames() / m_rate;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string Wave::getExtension() const
+{
+       return u::fs::getExt(m_path);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Wave::setRate(int v) { m_rate = v; }
+void Wave::setLogical(bool l) { m_logical = l; }
+void Wave::setEdited(bool e) { m_edited = e; }
+
+/* -------------------------------------------------------------------------- */
+
+void Wave::setPath(const std::string& p, int wid)
+{
+       if (wid == -1)
+               m_path = p;
+       else
+               m_path = u::fs::stripExt(p) + "-" + std::to_string(wid) + u::fs::getExt(p);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Wave::replaceData(mcl::AudioBuffer&& b)
+{
+       m_buffer = std::move(b);
+}
+} // namespace giada::m
diff --git a/src/core/wave.h b/src/core/wave.h
new file mode 100644 (file)
index 0000000..2e94af9
--- /dev/null
@@ -0,0 +1,89 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_WAVE_H
+#define G_WAVE_H
+
+#include "core/types.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include <string>
+
+namespace giada::m
+{
+class Wave
+{
+public:
+       Wave(ID id);
+       Wave(const Wave& o);
+       Wave(Wave&& o) = default;
+
+       Wave& operator=(Wave&& o) = default;
+
+       std::string getBasename(bool ext = false) const;
+       std::string getExtension() const;
+       int         getRate() const;
+       std::string getPath() const;
+       int         getBits() const;
+       int         getDuration() const;
+       bool        isLogical() const;
+       bool        isEdited() const;
+
+       /* getBuffer
+       Returns a (non-)const reference to the underlying audio buffer. */
+
+       mcl::AudioBuffer&       getBuffer();
+       const mcl::AudioBuffer& getBuffer() const;
+
+       /* setPath
+       Sets new path 'p'. If 'id' != -1 inserts a numeric id next to the file 
+       extension, e.g. : /path/to/sample-[id].wav */
+
+       void setPath(const std::string& p, int id = -1);
+
+       void setRate(int v);
+       void setLogical(bool l);
+       void setEdited(bool e);
+
+       /* replaceData
+       Replaces internal audio buffer with 'b' by moving it. */
+
+       void replaceData(mcl::AudioBuffer&& b);
+
+       void alloc(Frame size, int channels, int rate, int bits, const std::string& path);
+
+       ID id;
+
+private:
+       mcl::AudioBuffer m_buffer;
+       int              m_rate;
+       int              m_bits;
+       bool             m_logical; // memory only (a take)
+       bool             m_edited;  // edited via editor
+       std::string      m_path;    // E.g. /path/to/my/sample.wav
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/waveFx.cpp b/src/core/waveFx.cpp
new file mode 100644 (file)
index 0000000..7f5aa7e
--- /dev/null
@@ -0,0 +1,253 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "waveFx.h"
+#include "const.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include "utils/log.h"
+#include "wave.h"
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+
+namespace giada::m::wfx
+{
+namespace
+{
+void fadeFrame_(Wave& w, int i, float val)
+{
+       for (int j = 0; j < w.getBuffer().countChannels(); j++)
+               w.getBuffer()[i][j] *= val;
+}
+
+/* -------------------------------------------------------------------------- */
+
+float getPeak_(const Wave& w, int a, int b)
+{
+       float peak = 0.0f;
+       float abs  = 0.0f;
+       for (int i = a; i < b; i++)
+       {
+               for (int j = 0; j < w.getBuffer().countChannels(); j++) // Find highest value in any channel
+                       abs = fabs(w.getBuffer()[i][j]);
+               if (abs > peak)
+                       peak = abs;
+       }
+       return peak;
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+constexpr int SMOOTH_SIZE = 32;
+
+void normalize(Wave& w, int a, int b)
+{
+       float peak = getPeak_(w, a, b);
+       if (peak == 0.0f || peak > 1.0f)
+               return;
+
+       for (int i = a; i < b; i++)
+       {
+               for (int j = 0; j < w.getBuffer().countChannels(); j++)
+                       w.getBuffer()[i][j] = w.getBuffer()[i][j] * (1.0f / peak);
+       }
+       w.setEdited(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+int monoToStereo(Wave& w)
+{
+       if (w.getBuffer().countChannels() >= G_MAX_IO_CHANS)
+               return G_RES_OK;
+
+       mcl::AudioBuffer newData;
+       newData.alloc(w.getBuffer().countFrames(), G_MAX_IO_CHANS);
+
+       for (int i = 0; i < newData.countFrames(); i++)
+               for (int j = 0; j < newData.countChannels(); j++)
+                       newData[i][j] = w.getBuffer()[i][0];
+
+       w.replaceData(std::move(newData));
+
+       return G_RES_OK;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void silence(Wave& w, int a, int b)
+{
+       u::log::print("[wfx::silence] silencing from %d to %d\n", a, b);
+
+       for (int i = a; i < b; i++)
+               for (int j = 0; j < w.getBuffer().countChannels(); j++)
+                       w.getBuffer()[i][j] = 0.0f;
+       w.setEdited(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void cut(Wave& w, int a, int b)
+{
+       if (a < 0)
+               a = 0;
+       if (b > w.getBuffer().countFrames())
+               b = w.getBuffer().countFrames();
+
+       /* Create a new temp wave and copy there the original one, skipping the a-b
+    range. */
+
+       int newSize = w.getBuffer().countFrames() - (b - a);
+
+       mcl::AudioBuffer newData;
+       newData.alloc(newSize, w.getBuffer().countChannels());
+
+       u::log::print("[wfx::cut] cutting from %d to %d\n", a, b);
+
+       for (int i = 0, k = 0; i < w.getBuffer().countFrames(); i++)
+       {
+               if (i < a || i >= b)
+               {
+                       for (int j = 0; j < w.getBuffer().countChannels(); j++)
+                               newData[k][j] = w.getBuffer()[i][j];
+                       k++;
+               }
+       }
+
+       w.replaceData(std::move(newData));
+       w.setEdited(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void trim(Wave& w, Frame a, Frame b)
+{
+       if (a < 0)
+               a = 0;
+       if (b > w.getBuffer().countFrames())
+               b = w.getBuffer().countFrames();
+
+       Frame newSize = b - a;
+
+       mcl::AudioBuffer newData;
+       newData.alloc(newSize, w.getBuffer().countChannels());
+
+       u::log::print("[wfx::trim] trimming from %d to %d (area = %d)\n", a, b, b - a);
+
+       for (int i = 0; i < newData.countFrames(); i++)
+               for (int j = 0; j < newData.countChannels(); j++)
+                       newData[i][j] = w.getBuffer()[i + a][j];
+
+       w.replaceData(std::move(newData));
+       w.setEdited(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void paste(const Wave& src, Wave& des, Frame a)
+{
+       assert(src.getBuffer().countChannels() == des.getBuffer().countChannels());
+
+       mcl::AudioBuffer newData;
+       newData.alloc(src.getBuffer().countFrames() + des.getBuffer().countFrames(), des.getBuffer().countChannels());
+
+       /* |---original data---|///paste data///|---original data---|
+                des[0, a)      src[0, src.size)   des[a, des.size)     */
+
+       newData.set(des.getBuffer(), a, 0);
+       newData.set(src.getBuffer(), src.getBuffer().countFrames(), a);
+       newData.set(des.getBuffer(), des.getBuffer().countFrames() - a, src.getBuffer().countFrames() + a);
+
+       des.replaceData(std::move(newData));
+       des.setEdited(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void fade(Wave& w, int a, int b, Fade type)
+{
+       u::log::print("[wfx::fade] fade from %d to %d (range = %d)\n", a, b, b - a);
+
+       float m = 0.0f;
+       float d = 1.0f / (float)(b - a);
+
+       if (type == Fade::IN)
+               for (int i = a; i <= b; i++, m += d)
+                       fadeFrame_(w, i, m);
+       else
+               for (int i = b; i >= a; i--, m += d)
+                       fadeFrame_(w, i, m);
+
+       w.setEdited(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void smooth(Wave& w, int a, int b)
+{
+       /* Do nothing if fade edges (both of SMOOTH_SIZE samples) are > than selected 
+       portion of wave. SMOOTH_SIZE*2 to count both edges. */
+
+       if (SMOOTH_SIZE * 2 > (b - a))
+       {
+               u::log::print("[wfx::smooth] selection is too small, nothing to do\n");
+               return;
+       }
+
+       fade(w, a, a + SMOOTH_SIZE, Fade::IN);
+       fade(w, b - SMOOTH_SIZE, b, Fade::OUT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void shift(Wave& w, Frame offset)
+{
+       if (offset < 0)
+               offset = (w.getBuffer().countFrames() + w.getBuffer().countChannels()) + offset;
+
+       float* begin = w.getBuffer()[0];
+       float* end   = w.getBuffer()[0] + (w.getBuffer().countFrames() * w.getBuffer().countChannels());
+
+       std::rotate(begin, end - (offset * w.getBuffer().countChannels()), end);
+       w.setEdited(true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void reverse(Wave& w, Frame a, Frame b)
+{
+       /* https://stackoverflow.com/questions/33201528/reversing-an-array-of-structures-in-c */
+       float* begin = w.getBuffer()[0] + (a * w.getBuffer().countChannels());
+       float* end   = w.getBuffer()[0] + (b * w.getBuffer().countChannels());
+
+       std::reverse(begin, end);
+
+       w.setEdited(true);
+}
+} // namespace giada::m::wfx
diff --git a/src/core/waveFx.h b/src/core/waveFx.h
new file mode 100644 (file)
index 0000000..37bf462
--- /dev/null
@@ -0,0 +1,87 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_WAVE_FX_H
+#define G_WAVE_FX_H
+
+#include "core/types.h"
+
+namespace giada::m
+{
+class Wave;
+}
+
+namespace giada::m::wfx
+{
+/* Windows fix */
+#ifdef _WIN32
+#undef IN
+#undef OUT
+#endif
+enum class Fade
+{
+       IN,
+       OUT
+};
+
+/* monoToStereo
+Converts a 1-channel Wave to a 2-channels wave. */
+
+int monoToStereo(Wave& w);
+
+/* normalize
+Normalizes the wave in range a-b by altering values in memory. */
+
+void normalize(Wave& w, int a, int b);
+
+void silence(Wave& w, int a, int b);
+void cut(Wave& w, int a, int b);
+void trim(Wave& w, int a, int b);
+
+/* paste
+Pastes Wave 'src' into Wave 'dest', starting from frame 'a'. */
+
+void paste(const Wave& src, Wave& dest, Frame a);
+
+/* fade
+Fades in or fades out selection. Can be Fade::IN or Fade::OUT. */
+
+void fade(Wave& w, int a, int b, Fade type);
+
+/* smooth
+Smooth edges of selection. */
+
+void smooth(Wave& w, int a, int b);
+
+/* reverse
+Flips Wave's data. */
+
+void reverse(Wave& v, Frame a, Frame b);
+
+void shift(Wave& w, Frame offset);
+} // namespace giada::m::wfx
+
+#endif
diff --git a/src/core/waveManager.cpp b/src/core/waveManager.cpp
new file mode 100644 (file)
index 0000000..fbb16bb
--- /dev/null
@@ -0,0 +1,268 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "waveManager.h"
+#include "const.h"
+#include "deps/mcl-audio-buffer/src/audioBuffer.hpp"
+#include "idManager.h"
+#include "patch.h"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include "wave.h"
+#include "waveFx.h"
+#include <cmath>
+#include <memory>
+#include <samplerate.h>
+#include <sndfile.h>
+
+namespace giada::m
+{
+namespace
+{
+int getBits_(const SF_INFO& header)
+{
+       if (header.format & SF_FORMAT_PCM_S8)
+               return 8;
+       else if (header.format & SF_FORMAT_PCM_16)
+               return 16;
+       else if (header.format & SF_FORMAT_PCM_24)
+               return 24;
+       else if (header.format & SF_FORMAT_PCM_32)
+               return 32;
+       else if (header.format & SF_FORMAT_PCM_U8)
+               return 8;
+       else if (header.format & SF_FORMAT_FLOAT)
+               return 32;
+       else if (header.format & SF_FORMAT_DOUBLE)
+               return 64;
+       return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string makeWavePath_(const std::string& base, const m::Wave& w, int k)
+{
+       return base + G_SLASH + w.getBasename(/*ext=*/false) + "-" + std::to_string(k) + w.getExtension();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool isWavePathUnique_(const m::Wave& skip, const std::string& path,
+    const std::vector<std::unique_ptr<Wave>>& waves)
+{
+       for (const auto& w : waves)
+               if (w->id != skip.id && w->getPath() == path)
+                       return false;
+       return true;
+}
+
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+std::string makeUniqueWavePath(const std::string& base, const m::Wave& w,
+    const std::vector<std::unique_ptr<Wave>>& waves)
+{
+       std::string path = base + G_SLASH + w.getBasename(/*ext=*/true);
+       if (isWavePathUnique_(w, path, waves))
+               return path;
+
+       // TODO - just use a timestamp. e.g. makeWavePath_(..., ..., getTimeStamp())
+       int k = 0;
+       path  = makeWavePath_(base, w, k);
+       while (!isWavePathUnique_(w, path, waves))
+               path = makeWavePath_(base, w, k++);
+
+       return path;
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+void WaveManager::reset()
+{
+       m_waveId = IdManager();
+}
+
+/* -------------------------------------------------------------------------- */
+
+WaveManager::Result WaveManager::createFromFile(const std::string& path, ID id,
+    int samplerate, int quality)
+{
+       if (path == "" || u::fs::isDir(path))
+       {
+               u::log::print("[waveManager::create] malformed path (was '%s')\n", path);
+               return {G_RES_ERR_NO_DATA};
+       }
+
+       if (path.size() > FILENAME_MAX)
+               return {G_RES_ERR_PATH_TOO_LONG};
+
+       SF_INFO  header;
+       SNDFILE* fileIn = sf_open(path.c_str(), SFM_READ, &header);
+
+       if (fileIn == nullptr)
+       {
+               u::log::print("[waveManager::create] unable to read %s. %s\n", path, sf_strerror(fileIn));
+               return {G_RES_ERR_IO};
+       }
+
+       if (header.channels > G_MAX_IO_CHANS)
+       {
+               u::log::print("[waveManager::create] unsupported multi-channel sample\n");
+               return {G_RES_ERR_WRONG_DATA};
+       }
+
+       m_waveId.set(id);
+
+       std::unique_ptr<Wave> wave = std::make_unique<Wave>(m_waveId.generate(id));
+       wave->alloc(header.frames, header.channels, header.samplerate, getBits_(header), path);
+
+       if (sf_readf_float(fileIn, wave->getBuffer()[0], header.frames) != header.frames)
+               u::log::print("[waveManager::create] warning: incomplete read!\n");
+
+       sf_close(fileIn);
+
+       if (header.channels == 1 && !wfx::monoToStereo(*wave))
+               return {G_RES_ERR_PROCESSING};
+
+       if (wave->getRate() != samplerate)
+       {
+               u::log::print("[waveManager::create] input rate (%d) != required rate (%d), conversion needed\n",
+                   wave->getRate(), samplerate);
+               if (resample(*wave.get(), quality, samplerate) != G_RES_OK)
+                       return {G_RES_ERR_PROCESSING};
+       }
+
+       u::log::print("[waveManager::create] new Wave created, %d frames\n", wave->getBuffer().countFrames());
+
+       return {G_RES_OK, std::move(wave)};
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unique_ptr<Wave> WaveManager::createEmpty(int frames, int channels, int samplerate,
+    const std::string& name)
+{
+       std::unique_ptr<Wave> wave = std::make_unique<Wave>(m_waveId.generate());
+       wave->alloc(frames, channels, samplerate, G_DEFAULT_BIT_DEPTH, name);
+       wave->setLogical(true);
+
+       u::log::print("[waveManager::createEmpty] new empty Wave created, %d frames\n",
+           wave->getBuffer().countFrames());
+
+       return wave;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unique_ptr<Wave> WaveManager::createFromWave(const Wave& src, int a, int b)
+{
+       int channels = src.getBuffer().countChannels();
+       int frames   = b - a;
+
+       std::unique_ptr<Wave> wave = std::make_unique<Wave>(m_waveId.generate());
+       wave->alloc(frames, channels, src.getRate(), src.getBits(), src.getPath());
+       wave->getBuffer().set(src.getBuffer(), frames);
+       wave->setLogical(true);
+
+       u::log::print("[waveManager::createFromWave] new Wave created, %d frames\n", frames);
+
+       return wave;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::unique_ptr<Wave> WaveManager::deserializeWave(const Patch::Wave& w, int samplerate, int quality)
+{
+       return createFromFile(w.path, w.id, samplerate, quality).wave;
+}
+
+const Patch::Wave WaveManager::serializeWave(const Wave& w) const
+{
+       return {w.id, u::fs::basename(w.getPath())};
+}
+
+/* -------------------------------------------------------------------------- */
+
+int WaveManager::resample(Wave& w, int quality, int samplerate)
+{
+       float ratio         = samplerate / (float)w.getRate();
+       int   newSizeFrames = static_cast<int>(ceil(w.getBuffer().countFrames() * ratio));
+
+       mcl::AudioBuffer newData;
+       newData.alloc(newSizeFrames, w.getBuffer().countChannels());
+
+       SRC_DATA src_data;
+       src_data.data_in       = w.getBuffer()[0];
+       src_data.input_frames  = w.getBuffer().countFrames();
+       src_data.data_out      = newData[0];
+       src_data.output_frames = newSizeFrames;
+       src_data.src_ratio     = ratio;
+
+       u::log::print("[waveManager::resample] resampling: new size=%d frames\n", newSizeFrames);
+
+       int ret = src_simple(&src_data, quality, w.getBuffer().countChannels());
+       if (ret != 0)
+       {
+               u::log::print("[waveManager::resample] resampling error: %s\n", src_strerror(ret));
+               return G_RES_ERR_PROCESSING;
+       }
+
+       w.replaceData(std::move(newData));
+       w.setRate(samplerate);
+
+       return G_RES_OK;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int WaveManager::save(const Wave& w, const std::string& path)
+{
+       SF_INFO header;
+       header.samplerate = w.getRate();
+       header.channels   = w.getBuffer().countChannels();
+       header.format     = SF_FORMAT_WAV | SF_FORMAT_FLOAT;
+
+       SNDFILE* file = sf_open(path.c_str(), SFM_WRITE, &header);
+       if (file == nullptr)
+       {
+               u::log::print("[waveManager::save] unable to open %s for exporting: %s\n",
+                   path, sf_strerror(file));
+               return G_RES_ERR_IO;
+       }
+
+       if (sf_writef_float(file, w.getBuffer()[0], w.getBuffer().countFrames()) != w.getBuffer().countFrames())
+               u::log::print("[waveManager::save] warning: incomplete write!\n");
+
+       sf_close(file);
+
+       return G_RES_OK;
+}
+} // namespace giada::m
diff --git a/src/core/waveManager.h b/src/core/waveManager.h
new file mode 100644 (file)
index 0000000..a50f8ec
--- /dev/null
@@ -0,0 +1,98 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_WAVE_MANAGER_H
+#define G_WAVE_MANAGER_H
+
+#include "core/idManager.h"
+#include "core/patch.h"
+#include "core/types.h"
+#include "core/wave.h"
+#include <memory>
+#include <string>
+
+namespace giada::m
+{
+std::string makeUniqueWavePath(const std::string& base, const m::Wave& w,
+    const std::vector<std::unique_ptr<Wave>>& waves);
+
+/* -------------------------------------------------------------------------- */
+
+class WaveManager final
+{
+public:
+       struct Result
+       {
+               int                   status;
+               std::unique_ptr<Wave> wave = nullptr;
+       };
+
+       /* reset
+    Resets internal ID generator. */
+
+       void reset();
+
+       /* create
+       Creates a new Wave object with data read from file 'path'. Pass id = 0 to 
+       auto-generate it. The function converts the Wave sample rate if it doesn't 
+       match the desired one as specified in 'samplerate'. */
+
+       Result createFromFile(const std::string& path, ID id, int samplerate, int quality);
+
+       /* createEmpty
+       Creates a new silent Wave object. */
+
+       std::unique_ptr<Wave> createEmpty(int frames, int channels, int samplerate,
+           const std::string& name);
+
+       /* createFromWave
+       Creates a new Wave from an existing one, copying the data in range a - b. */
+
+       std::unique_ptr<Wave> createFromWave(const Wave& src, int a, int b);
+
+       /* (de)serializeWave
+       Creates a new Wave given the patch raw data and vice versa. */
+
+       std::unique_ptr<Wave> deserializeWave(const Patch::Wave& w, int samplerate, int quality);
+       const Patch::Wave     serializeWave(const Wave& w) const;
+
+       /* resample
+       Change sample rate of 'w' to the desider value. The 'quality' parameter sets 
+       the algorithm to use for the conversion. */
+
+       int resample(Wave& w, int quality, int samplerate);
+
+       /* save
+       Writes Wave data to file 'path'. Only 'wav' format is supported for now. */
+
+       int save(const Wave& w, const std::string& path);
+
+private:
+       IdManager m_waveId;
+};
+} // namespace giada::m
+
+#endif
diff --git a/src/core/weakAtomic.h b/src/core/weakAtomic.h
new file mode 100644 (file)
index 0000000..f2bca23
--- /dev/null
@@ -0,0 +1,87 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_WEAK_ATOMIC_H
+#define G_WEAK_ATOMIC_H
+
+#include <atomic>
+#include <functional>
+
+namespace giada
+{
+template <typename T>
+class WeakAtomic
+{
+public:
+       WeakAtomic() = default;
+
+       WeakAtomic(T t)
+       : m_atomic(t)
+       , m_value(t)
+       {
+       }
+
+       WeakAtomic(const WeakAtomic& o)
+       : m_atomic(o.load())
+       , m_value(o.m_value)
+       {
+       }
+
+       WeakAtomic(WeakAtomic&& o) = delete;
+
+       WeakAtomic& operator=(const WeakAtomic& o)
+       {
+               if (this == &o)
+                       return *this;
+               store(o.load());
+               m_value = o.m_value;
+               return *this;
+       }
+
+       WeakAtomic& operator=(WeakAtomic&& o) = delete;
+
+       T load() const
+       {
+               return m_atomic.load(std::memory_order_relaxed);
+       }
+
+       void store(T t)
+       {
+               m_atomic.store(t, std::memory_order_relaxed);
+               if (onChange != nullptr && t != m_value)
+                       onChange(t);
+               m_value = t;
+       }
+
+       std::function<void(T)> onChange = nullptr;
+
+private:
+       std::atomic<T> m_atomic;
+       T              m_value;
+};
+} // namespace giada
+
+#endif
\ No newline at end of file
diff --git a/src/core/worker.cpp b/src/core/worker.cpp
new file mode 100644 (file)
index 0000000..8fef05b
--- /dev/null
@@ -0,0 +1,66 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "worker.h"
+#include "utils/time.h"
+
+namespace giada
+{
+Worker::Worker()
+: m_running(false)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+Worker::~Worker()
+{
+       stop();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Worker::start(std::function<void()> f, int sleep)
+{
+       m_running.store(true);
+       m_thread = std::thread([this, f, sleep]() {
+               while (m_running.load() == true)
+               {
+                       f();
+                       u::time::sleep(sleep);
+               }
+       });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Worker::stop()
+{
+       m_running.store(false);
+       if (m_thread.joinable())
+               m_thread.join();
+}
+} // namespace giada
diff --git a/src/core/worker.h b/src/core/worker.h
new file mode 100644 (file)
index 0000000..aad217d
--- /dev/null
@@ -0,0 +1,51 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2020 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_WORKER_H
+#define G_WORKER_H
+
+#include <atomic>
+#include <functional>
+#include <thread>
+
+namespace giada
+{
+class Worker
+{
+public:
+       Worker();
+       ~Worker();
+
+       void start(std::function<void()> f, int sleep);
+       void stop();
+
+  private:
+       std::thread       m_thread;
+       std::atomic<bool> m_running;
+};
+} // namespace giada
+
+#endif
\ No newline at end of file
diff --git a/src/deps/juce-config.h b/src/deps/juce-config.h
new file mode 100644 (file)
index 0000000..337a4a3
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef JUCE_APPCONFIG_H
+#define JUCE_APPCONFIG_H
+
+
+#include "juce/modules/juce_audio_basics/juce_audio_basics.h"
+#include "juce/modules/juce_audio_processors/juce_audio_processors.h"
+#include "juce/modules/juce_core/juce_core.h"
+#include "juce/modules/juce_data_structures/juce_data_structures.h"
+#include "juce/modules/juce_events/juce_events.h"
+#include "juce/modules/juce_graphics/juce_graphics.h"
+#include "juce/modules/juce_gui_basics/juce_gui_basics.h"
+#include "juce/modules/juce_gui_extra/juce_gui_extra.h"
+
+
+#endif
diff --git a/src/ext/giada.ico b/src/ext/giada.ico
new file mode 100644 (file)
index 0000000..06ac502
Binary files /dev/null and b/src/ext/giada.ico differ
diff --git a/src/ext/resource.h b/src/ext/resource.h
new file mode 100644 (file)
index 0000000..1dc4838
--- /dev/null
@@ -0,0 +1 @@
+#define IDI_ICON1 101
diff --git a/src/ext/resource.rc b/src/ext/resource.rc
new file mode 100644 (file)
index 0000000..fb0be40
--- /dev/null
@@ -0,0 +1,2 @@
+#include "resource.h"
+IDI_ICON1 ICON DISCARDABLE "giada.ico"
\ No newline at end of file
diff --git a/src/glue/actionEditor.cpp b/src/glue/actionEditor.cpp
new file mode 100644 (file)
index 0000000..7d2441c
--- /dev/null
@@ -0,0 +1,347 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "glue/actionEditor.h"
+#include "core/actions/action.h"
+#include "core/actions/actionRecorder.h"
+#include "core/actions/actions.h"
+#include "core/const.h"
+#include "core/engine.h"
+#include "core/model/model.h"
+#include "core/sequencer.h"
+#include "glue/events.h"
+#include "glue/recorder.h"
+#include <cassert>
+
+extern giada::m::Engine g_engine;
+
+namespace giada::c::actionEditor
+{
+namespace
+{
+Frame fixVerticalEnvActions_(Frame f, const m::Action& a1, const m::Action& a2)
+{
+       if (a1.frame == f)
+               f += 1;
+       else if (a2.frame == f)
+               f -= 1;
+       if (a1.frame == f || a2.frame == f)
+               return -1;
+       return f;
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* recordFirstEnvelopeAction_
+First action ever? Add actions at boundaries. */
+
+void recordFirstEnvelopeAction_(ID channelId, Frame frame, int value)
+{
+       // TODO - use MidiEvent(float)
+       m::MidiEvent    e1 = m::MidiEvent(m::MidiEvent::ENVELOPE, 0, G_MAX_VELOCITY);
+       m::MidiEvent    e2 = m::MidiEvent(m::MidiEvent::ENVELOPE, 0, value);
+       const m::Action a1 = g_engine.actionRecorder.rec(channelId, 0, e1);
+       const m::Action a2 = g_engine.actionRecorder.rec(channelId, frame, e2);
+       const m::Action a3 = g_engine.actionRecorder.rec(channelId, g_engine.sequencer.getFramesInLoop() - 1, e1);
+
+       g_engine.actionRecorder.updateSiblings(a1.id, /*prev=*/a3.id, /*next=*/a2.id); // Circular loop (begin)
+       g_engine.actionRecorder.updateSiblings(a2.id, /*prev=*/a1.id, /*next=*/a3.id);
+       g_engine.actionRecorder.updateSiblings(a3.id, /*prev=*/a2.id, /*next=*/a1.id); // Circular loop (end)
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* recordNonFirstEnvelopeAction_
+Find action right before frame 'frame' and inject a new action in there. 
+Vertical envelope points are forbidden. */
+
+void recordNonFirstEnvelopeAction_(ID channelId, Frame frame, int value)
+{
+       const m::Action a1 = g_engine.actionRecorder.getClosestAction(channelId, frame, m::MidiEvent::ENVELOPE);
+       const m::Action a3 = a1.next != nullptr ? *a1.next : m::Action{};
+
+       assert(a1.isValid());
+       assert(a3.isValid());
+
+       frame = fixVerticalEnvActions_(frame, a1, a3);
+       if (frame == -1) // Vertical points, nothing to do here
+               return;
+
+       // TODO - use MidiEvent(float)
+       m::MidiEvent    e2 = m::MidiEvent(m::MidiEvent::ENVELOPE, 0, value);
+       const m::Action a2 = g_engine.actionRecorder.rec(channelId, frame, e2);
+
+       g_engine.actionRecorder.updateSiblings(a2.id, a1.id, a3.id);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool isSinglePressMode_(ID channelId)
+{
+       /* TODO - use m::model getChannel utils (to be added) */
+       return g_engine.model.get().getChannel(channelId).samplePlayer->mode == SamplePlayerMode::SINGLE_PRESS;
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+SampleData::SampleData(const m::SamplePlayer& s)
+: channelMode(s.mode)
+, isLoopMode(s.isAnyLoopMode())
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+Data::Data(const m::Channel& c)
+: channelId(c.id)
+, channelName(c.name)
+, framesInSeq(g_engine.sequencer.getFramesInSeq())
+, framesInBeat(g_engine.sequencer.getFramesInBeat())
+, framesInBar(g_engine.sequencer.getFramesInBar())
+, framesInLoop(g_engine.sequencer.getFramesInLoop())
+, actions(g_engine.actionRecorder.getActionsOnChannel(c.id))
+{
+       if (c.type == ChannelType::SAMPLE)
+               sample = std::make_optional<SampleData>(c.samplePlayer.value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame Data::getCurrentFrame() const
+{
+       return g_engine.sequencer.getCurrentFrame();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Data::isChannelPlaying() const
+{
+       return g_engine.model.get().getChannel(channelId).isPlaying();
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Data getData(ID channelId)
+{
+       return Data(g_engine.model.get().getChannel(channelId));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void recordMidiAction(ID channelId, int note, int velocity, Frame f1, Frame f2)
+{
+       if (f2 == 0)
+               f2 = f1 + G_DEFAULT_ACTION_SIZE;
+
+       /* Avoid frame overflow. */
+
+       Frame overflow = f2 - (g_engine.sequencer.getFramesInLoop());
+       if (overflow > 0)
+       {
+               f2 -= overflow;
+               f1 -= overflow;
+       }
+
+       m::MidiEvent e1 = m::MidiEvent(m::MidiEvent::NOTE_ON, note, velocity);
+       m::MidiEvent e2 = m::MidiEvent(m::MidiEvent::NOTE_OFF, note, velocity);
+
+       g_engine.actionRecorder.rec(channelId, f1, f2, e1, e2);
+
+       recorder::updateChannel(channelId, /*updateActionEditor=*/false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void deleteMidiAction(ID channelId, const m::Action& a)
+{
+       assert(a.isValid());
+       assert(a.event.getStatus() == m::MidiEvent::NOTE_ON);
+
+       /* Send a note-off first in case we are deleting it in a middle of a 
+       key_on/key_off sequence. Check if 'next' exist first: could be orphaned. */
+
+       if (a.next != nullptr)
+       {
+               events::sendMidiToChannel(channelId, a.next->event, Thread::MAIN);
+               g_engine.actionRecorder.deleteAction(a.id, a.next->id);
+       }
+       else
+               g_engine.actionRecorder.deleteAction(a.id);
+
+       recorder::updateChannel(channelId, /*updateActionEditor=*/false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void updateMidiAction(ID channelId, const m::Action& a, int note, int velocity,
+    Frame f1, Frame f2)
+{
+       g_engine.actionRecorder.deleteAction(a.id, a.next->id);
+       recordMidiAction(channelId, note, velocity, f1, f2);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void recordSampleAction(ID channelId, int type, Frame f1, Frame f2)
+{
+       if (isSinglePressMode_(channelId))
+       {
+               if (f2 == 0)
+                       f2 = f1 + G_DEFAULT_ACTION_SIZE;
+               m::MidiEvent e1 = m::MidiEvent(m::MidiEvent::NOTE_ON, 0, 0);
+               m::MidiEvent e2 = m::MidiEvent(m::MidiEvent::NOTE_OFF, 0, 0);
+               g_engine.actionRecorder.rec(channelId, f1, f2, e1, e2);
+       }
+       else
+       {
+               m::MidiEvent e1 = m::MidiEvent(type, 0, 0);
+               g_engine.actionRecorder.rec(channelId, f1, e1);
+       }
+
+       recorder::updateChannel(channelId, /*updateActionEditor=*/false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void updateSampleAction(ID channelId, const m::Action& a, int type,
+    Frame f1, Frame f2)
+{
+       if (isSinglePressMode_(channelId))
+               g_engine.actionRecorder.deleteAction(a.id, a.next->id);
+       else
+               g_engine.actionRecorder.deleteAction(a.id);
+
+       recordSampleAction(channelId, type, f1, f2);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void deleteSampleAction(ID channelId, const m::Action& a)
+{
+       if (a.next != nullptr) // For ChannelMode::SINGLE_PRESS combo
+               g_engine.actionRecorder.deleteAction(a.id, a.next->id);
+       else
+               g_engine.actionRecorder.deleteAction(a.id);
+
+       recorder::updateChannel(channelId, /*updateActionEditor=*/false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void recordEnvelopeAction(ID channelId, Frame f, int value)
+{
+       assert(value >= 0 && value <= G_MAX_VELOCITY);
+
+       /* First action ever? Add actions at boundaries. Else, find action right
+       before frame 'f' and inject a new action in there. Vertical envelope points 
+       are forbidden for now. */
+
+       if (!g_engine.actionRecorder.hasActions(channelId, m::MidiEvent::ENVELOPE))
+               recordFirstEnvelopeAction_(channelId, f, value);
+       else
+               recordNonFirstEnvelopeAction_(channelId, f, value);
+
+       recorder::updateChannel(channelId, /*updateActionEditor=*/false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void deleteEnvelopeAction(ID channelId, const m::Action& a)
+{
+       /* Deleting a boundary action wipes out everything. If is volume, remember 
+       to restore _i and _d members in channel. */
+       /* TODO - move this to c::*/
+       /* TODO - FIX*/
+       if (g_engine.actionRecorder.isBoundaryEnvelopeAction(a))
+       {
+               if (a.isVolumeEnvelope())
+               {
+                       // TODO reset all volume vars to 1.0
+               }
+               g_engine.actionRecorder.clearActions(channelId, a.event.getStatus());
+       }
+       else
+       {
+               assert(a.prev != nullptr);
+               assert(a.next != nullptr);
+
+               const m::Action a1     = *a.prev;
+               const m::Action a1prev = *a1.prev;
+               const m::Action a3     = *a.next;
+               const m::Action a3next = *a3.next;
+
+               /* Original status:   a1--->a--->a3
+                  Modified status:   a1-------->a3 
+               Order is important, here: first update siblings, then delete the action.
+               Otherwise ActionRecorder::deleteAction() would complain of missing 
+               prevId/nextId no longer found. */
+
+               g_engine.actionRecorder.updateSiblings(a1.id, a1prev.id, a3.id);
+               g_engine.actionRecorder.updateSiblings(a3.id, a1.id, a3next.id);
+               g_engine.actionRecorder.deleteAction(a.id);
+       }
+
+       recorder::updateChannel(channelId, /*updateActionEditor=*/false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void updateEnvelopeAction(ID channelId, const m::Action& a, Frame f, int value)
+{
+       /* Update the action directly if it is a boundary one. Else, delete the
+       previous one and record a new action. */
+
+       if (g_engine.actionRecorder.isBoundaryEnvelopeAction(a))
+               g_engine.actionRecorder.updateEvent(a.id, m::MidiEvent(m::MidiEvent::ENVELOPE, 0, value));
+       else
+       {
+               deleteEnvelopeAction(channelId, a);
+               recordEnvelopeAction(channelId, f, value);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<m::Action> getActions(ID channelId)
+{
+       return g_engine.actionRecorder.getActionsOnChannel(channelId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void updateVelocity(const m::Action& a, int value)
+{
+       m::MidiEvent event(a.event);
+       event.setVelocity(value);
+
+       g_engine.actionRecorder.updateEvent(a.id, event);
+}
+} // namespace giada::c::actionEditor
diff --git a/src/glue/actionEditor.h b/src/glue/actionEditor.h
new file mode 100644 (file)
index 0000000..d624e2a
--- /dev/null
@@ -0,0 +1,96 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_ACTION_EDITOR_H
+#define G_GLUE_ACTION_EDITOR_H
+
+#include "core/types.h"
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace giada::m
+{
+struct Action;
+class SamplePlayer;
+class Channel;
+} // namespace giada::m
+
+namespace giada::c::actionEditor
+{
+struct SampleData
+{
+       SampleData(const m::SamplePlayer&);
+
+       SamplePlayerMode channelMode;
+       bool             isLoopMode;
+};
+
+struct Data
+{
+       Data() = default;
+       Data(const m::Channel&);
+
+       Frame getCurrentFrame() const;
+       bool  isChannelPlaying() const;
+
+       ID                     channelId;
+       std::string            channelName;
+       Frame                  framesInSeq;
+       Frame                  framesInBeat;
+       Frame                  framesInBar;
+       Frame                  framesInLoop;
+       std::vector<m::Action> actions;
+
+       std::optional<SampleData> sample;
+};
+
+Data getData(ID channelId);
+
+/* MIDI actions.  */
+
+void recordMidiAction(ID channelId, int note, int velocity, Frame f1,
+    Frame f2 = 0);
+void deleteMidiAction(ID channelId, const m::Action& a);
+void updateMidiAction(ID channelId, const m::Action& a, int note, int velocity,
+    Frame f1, Frame f2);
+void updateVelocity(const m::Action& a, int value);
+
+/* Sample Actions. */
+
+void recordSampleAction(ID channelId, int type, Frame f1, Frame f2 = 0);
+void deleteSampleAction(ID channelId, const m::Action& a);
+void updateSampleAction(ID channelId, const m::Action& a, int type,
+    Frame f1, Frame f2 = 0);
+
+/* Envelope actions (only volume for now). */
+
+void recordEnvelopeAction(ID channelId, Frame f, int value);
+void deleteEnvelopeAction(ID channelId, const m::Action& a);
+void updateEnvelopeAction(ID channelId, const m::Action& a, Frame f, int value);
+} // namespace giada::c::actionEditor
+
+#endif
diff --git a/src/glue/channel.cpp b/src/glue/channel.cpp
new file mode 100644 (file)
index 0000000..2e25bc2
--- /dev/null
@@ -0,0 +1,334 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "core/actions/actionRecorder.h"
+#include "core/channels/channelManager.h"
+#include "core/conf.h"
+#include "core/engine.h"
+#include "core/kernelAudio.h"
+#include "core/mixer.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/patch.h"
+#include "core/plugins/plugin.h"
+#include "core/plugins/pluginHost.h"
+#include "core/plugins/pluginManager.h"
+#include "core/recorder.h"
+#include "core/wave.h"
+#include "core/waveManager.h"
+#include "glue/channel.h"
+#include "glue/main.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/dispatcher.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/mainWindow/keyboard/channelButton.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/mainWindow/keyboard/sampleChannel.h"
+#include "gui/elems/sampleEditor/boostTool.h"
+#include "gui/elems/sampleEditor/panTool.h"
+#include "gui/elems/sampleEditor/pitchTool.h"
+#include "gui/elems/sampleEditor/rangeTool.h"
+#include "gui/elems/sampleEditor/volumeTool.h"
+#include "gui/elems/sampleEditor/waveTools.h"
+#include "gui/elems/sampleEditor/waveform.h"
+#include "gui/ui.h"
+#include "src/core/actions/actions.h"
+#include "utils/fs.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include <FL/Fl.H>
+#include <cassert>
+#include <cmath>
+#include <functional>
+
+extern giada::v::Ui     g_ui;
+extern giada::m::Engine g_engine;
+
+namespace giada::c::channel
+{
+namespace
+{
+void printLoadError_(int res)
+{
+       if (res == G_RES_ERR_WRONG_DATA)
+               v::gdAlert("Multichannel samples not supported.");
+       else if (res == G_RES_ERR_IO)
+               v::gdAlert("Unable to read this sample.");
+       else if (res == G_RES_ERR_PATH_TOO_LONG)
+               v::gdAlert("File path too long.");
+       else if (res == G_RES_ERR_NO_DATA)
+               v::gdAlert("No file specified.");
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+// TODO - just pass const Channel&
+SampleData::SampleData(const m::Channel& ch)
+: waveId(ch.samplePlayer->getWaveId())
+, mode(ch.samplePlayer->mode)
+, isLoop(ch.samplePlayer->isAnyLoopMode())
+, pitch(ch.samplePlayer->pitch)
+, m_channel(&ch)
+{
+}
+
+Frame SampleData::getTracker() const { return m_channel->shared->tracker.load(); }
+/* TODO - useless methods, turn them into member vars */
+Frame SampleData::getBegin() const { return m_channel->samplePlayer->begin; }
+Frame SampleData::getEnd() const { return m_channel->samplePlayer->end; }
+bool  SampleData::getInputMonitor() const { return m_channel->audioReceiver->inputMonitor; }
+bool  SampleData::getOverdubProtection() const { return m_channel->audioReceiver->overdubProtection; }
+
+/* -------------------------------------------------------------------------- */
+
+MidiData::MidiData(const m::Channel& m)
+: m_channel(&m)
+{
+}
+
+/* TODO - useless methods, turn them into member vars */
+bool MidiData::isOutputEnabled() const { return m_channel->midiSender->enabled; }
+int  MidiData::getFilter() const { return m_channel->midiSender->filter; }
+
+/* -------------------------------------------------------------------------- */
+
+Data::Data(const m::Channel& c)
+: viewDispatcher(g_ui.dispatcher)
+, id(c.id)
+, columnId(c.columnId)
+#ifdef WITH_VST
+, plugins(c.plugins)
+#endif
+, type(c.type)
+, height(c.height)
+, name(c.name)
+, volume(c.volume)
+, pan(c.pan)
+, key(c.key)
+, hasActions(c.hasActions)
+, m_channel(c)
+{
+       if (c.type == ChannelType::SAMPLE)
+               sample = std::make_optional<SampleData>(c);
+       else if (c.type == ChannelType::MIDI)
+               midi = std::make_optional<MidiData>(c);
+}
+
+ChannelStatus Data::getPlayStatus() const { return m_channel.shared->playStatus.load(); }
+ChannelStatus Data::getRecStatus() const { return m_channel.shared->recStatus.load(); }
+bool          Data::getReadActions() const { return m_channel.shared->readActions.load(); }
+bool          Data::isRecordingInput() const { return g_engine.recorder.isRecordingInput(); }
+bool          Data::isRecordingAction() const { return g_engine.recorder.isRecordingAction(); }
+/* TODO - useless methods, turn them into member vars */
+bool Data::getSolo() const { return m_channel.isSoloed(); }
+bool Data::getMute() const { return m_channel.isMuted(); }
+bool Data::isArmed() const { return m_channel.armed; }
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Data getData(ID channelId)
+{
+       return Data(g_engine.model.get().getChannel(channelId));
+}
+
+std::vector<Data> getChannels()
+{
+       std::vector<Data> out;
+       for (const m::Channel& ch : g_engine.model.get().channels)
+               if (!ch.isInternal())
+                       out.push_back(Data(ch));
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int loadChannel(ID channelId, const std::string& fname)
+{
+       auto progress = g_ui.mainWindow->getScopedProgress("Loading sample...");
+
+       m::WaveManager::Result res = g_engine.waveManager.createFromFile(fname, /*id=*/0,
+           g_engine.kernelAudio.getSampleRate(), g_engine.conf.data.rsmpQuality);
+
+       if (res.status != G_RES_OK)
+       {
+               printLoadError_(res.status);
+               return res.status;
+       }
+
+       /* Save the patch and take the last browser's dir in order to re-use it the 
+       next time. */
+
+       g_engine.conf.data.samplePath = u::fs::dirname(fname);
+       g_engine.mixerHandler.loadChannel(channelId, std::move(res.wave));
+       return G_RES_OK;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void addChannel(ID columnId, ChannelType type)
+{
+       m::Channel& ch = g_engine.mixerHandler.addChannel(type, columnId,
+           g_engine.kernelAudio.getBufferSize(), g_engine.channelManager);
+
+       auto onSendMidiCb = [channelId = ch.id]() { g_ui.mainWindow->keyboard->notifyMidiOut(channelId); };
+
+       ch.midiLighter.onSend = onSendMidiCb;
+       if (ch.midiSender)
+               ch.midiSender->onSend = onSendMidiCb;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void addAndLoadChannels(ID columnId, const std::vector<std::string>& fnames)
+{
+       auto progress = g_ui.mainWindow->getScopedProgress("Loading samples...");
+
+       bool errors = false;
+       int  i      = 0;
+       for (const std::string& f : fnames)
+       {
+               progress.get().setProgress(++i / static_cast<float>(fnames.size()));
+
+               m::WaveManager::Result res = g_engine.waveManager.createFromFile(f, /*id=*/0,
+                   g_engine.kernelAudio.getSampleRate(), g_engine.conf.data.rsmpQuality);
+               if (res.status == G_RES_OK)
+                       g_engine.mixerHandler.addAndLoadChannel(columnId, std::move(res.wave), g_engine.kernelAudio.getBufferSize(),
+                           g_engine.channelManager);
+               else
+                       errors = true;
+       }
+
+       if (errors)
+               v::gdAlert("Some files weren't loaded successfully.");
+}
+
+/* -------------------------------------------------------------------------- */
+
+void deleteChannel(ID channelId)
+{
+       if (!v::gdConfirmWin("Warning", "Delete channel: are you sure?"))
+               return;
+       g_ui.closeAllSubwindows();
+
+#ifdef WITH_VST
+       const std::vector<m::Plugin*> plugins = g_engine.model.get().getChannel(channelId).plugins;
+#endif
+       g_engine.mixerHandler.deleteChannel(channelId);
+       g_engine.actionRecorder.clearChannel(channelId);
+#ifdef WITH_VST
+       g_engine.pluginHost.freePlugins(plugins);
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+void freeChannel(ID channelId)
+{
+       if (!v::gdConfirmWin("Warning", "Free channel: are you sure?"))
+               return;
+       g_ui.closeAllSubwindows();
+       g_engine.actionRecorder.clearChannel(channelId);
+       g_engine.mixerHandler.freeChannel(channelId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setInputMonitor(ID channelId, bool value)
+{
+       g_engine.model.get().getChannel(channelId).audioReceiver->inputMonitor = value;
+       g_engine.model.swap(m::model::SwapType::SOFT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setOverdubProtection(ID channelId, bool value)
+{
+       m::Channel& ch                      = g_engine.model.get().getChannel(channelId);
+       ch.audioReceiver->overdubProtection = value;
+       if (value == true && ch.armed)
+               ch.armed = false;
+       g_engine.model.swap(m::model::SwapType::SOFT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void cloneChannel(ID channelId)
+{
+       g_engine.actionRecorder.cloneActions(channelId, g_engine.channelManager.getNextId());
+#ifdef WITH_VST
+       g_engine.mixerHandler.cloneChannel(channelId, g_engine.patch.data.samplerate,
+           g_engine.kernelAudio.getBufferSize(), g_engine.channelManager, g_engine.waveManager,
+           g_engine.sequencer, g_engine.pluginManager);
+#else
+       g_engine.mixerHandler.cloneChannel(channelId, g_engine.kernelAudio.getBufferSize(),
+           g_engine.channelManager, g_engine.waveManager);
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setSamplePlayerMode(ID channelId, SamplePlayerMode mode)
+{
+       g_engine.model.get().getChannel(channelId).samplePlayer->mode = mode;
+       g_engine.model.swap(m::model::SwapType::HARD); // TODO - SOFT should be enough, fix geChannel refresh method
+       g_ui.refreshSubWindow(WID_ACTION_EDITOR);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setHeight(ID channelId, Pixel p)
+{
+       g_engine.model.get().getChannel(channelId).height = p;
+       g_engine.model.swap(m::model::SwapType::SOFT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setName(ID channelId, const std::string& name)
+{
+       g_engine.mixerHandler.renameChannel(channelId, name);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setCallbacks(m::Channel& ch)
+{
+       auto onSendMidiCb = [channelId = ch.id]() { g_ui.mainWindow->keyboard->notifyMidiOut(channelId); };
+
+       ch.midiLighter.onSend = onSendMidiCb;
+       if (ch.midiSender)
+               ch.midiSender->onSend = onSendMidiCb;
+}
+} // namespace giada::c::channel
diff --git a/src/glue/channel.h b/src/glue/channel.h
new file mode 100644 (file)
index 0000000..c98e01d
--- /dev/null
@@ -0,0 +1,173 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_CHANNEL_H
+#define G_GLUE_CHANNEL_H
+
+#include "core/model/model.h"
+#include "core/types.h"
+#include <atomic>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace giada::m
+{
+class Plugin;
+}
+
+namespace giada::v
+{
+class Dispatcher;
+}
+
+namespace giada::c::channel
+{
+struct SampleData
+{
+       SampleData() = delete;
+       SampleData(const m::Channel&);
+
+       Frame getTracker() const;
+       Frame getBegin() const;
+       Frame getEnd() const;
+       bool  getInputMonitor() const;
+       bool  getOverdubProtection() const;
+
+       ID               waveId;
+       SamplePlayerMode mode;
+       bool             isLoop;
+       float            pitch;
+
+private:
+       const m::Channel* m_channel;
+};
+
+struct MidiData
+{
+       MidiData() = delete;
+       MidiData(const m::Channel&);
+
+       bool isOutputEnabled() const;
+       int  getFilter() const;
+
+private:
+       const m::Channel* m_channel;
+};
+
+struct Data
+{
+       Data(const m::Channel&);
+
+       bool          getMute() const;
+       bool          getSolo() const;
+       ChannelStatus getPlayStatus() const;
+       ChannelStatus getRecStatus() const;
+       bool          getReadActions() const;
+       bool          isArmed() const;
+       bool          isRecordingInput() const;
+       bool          isRecordingAction() const;
+
+       v::Dispatcher& viewDispatcher;
+
+       ID id;
+       ID columnId;
+#ifdef WITH_VST
+       std::vector<m::Plugin*> plugins;
+#endif
+       ChannelType type;
+       Pixel       height;
+       std::string name;
+       float       volume;
+       float       pan;
+       int         key;
+       bool        hasActions;
+
+       std::optional<SampleData> sample;
+       std::optional<MidiData>   midi;
+
+private:
+       const m::Channel& m_channel;
+};
+
+/* getChannels
+Returns a single viewModel object filled with data from a channel. */
+
+Data getData(ID channelId);
+
+/* getChannels
+Returns a vector of viewModel objects filled with data from channels. */
+
+std::vector<Data> getChannels();
+
+/* addChannel
+Adds an empty new channel to the stack. */
+
+void addChannel(ID columnId, ChannelType type);
+
+/* loadChannel
+Fills an existing channel with a wave. */
+
+int loadChannel(ID columnId, const std::string& fname);
+
+/* addAndLoadChannels
+As above, with multiple audio file paths in input. */
+
+void addAndLoadChannels(ID columnId, const std::vector<std::string>& fpaths);
+
+/* deleteChannel
+Removes a channel from Mixer. */
+
+void deleteChannel(ID channelId);
+
+/* freeChannel
+Unloads the sample from a sample channel. */
+
+void freeChannel(ID channelId);
+
+/* cloneChannel
+Makes an exact copy of a channel. */
+
+void cloneChannel(ID channelId);
+
+/* set*
+Sets several channel properties. */
+
+void setInputMonitor(ID channelId, bool value);
+void setOverdubProtection(ID channelId, bool value);
+void setName(ID channelId, const std::string& name);
+void setHeight(ID channelId, Pixel p);
+
+void setSamplePlayerMode(ID channelId, SamplePlayerMode m);
+
+/* setCallbacks
+Install callbacks to a m::Channel object in order to communicate with the UI. 
+Call this whenever you add a new channel. */
+
+void setCallbacks(m::Channel&);
+} // namespace giada::c::channel
+
+#endif
diff --git a/src/glue/config.cpp b/src/glue/config.cpp
new file mode 100644 (file)
index 0000000..db2c926
--- /dev/null
@@ -0,0 +1,332 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "glue/config.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/engine.h"
+#include "core/kernelAudio.h"
+#include "core/kernelMidi.h"
+#include "core/midiMapper.h"
+#include "core/plugins/pluginManager.h"
+#include "deps/rtaudio/RtAudio.h"
+#include "gui/dialogs/browser/browserDir.h"
+#include "gui/dialogs/config.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/config/tabPlugins.h"
+#include "gui/ui.h"
+#include "utils/fs.h"
+#include "utils/vector.h"
+#include <FL/Fl_Tooltip.H>
+#include <cstddef>
+
+extern giada::v::Ui     g_ui;
+extern giada::m::Engine g_engine;
+
+namespace giada::c::config
+{
+namespace
+{
+AudioDeviceData getAudioDeviceData_(DeviceType type, size_t index, int channelsCount, int channelsStart)
+{
+       for (const m::KernelAudio::Device& device : g_engine.kernelAudio.getDevices())
+               if (device.index == index)
+                       return AudioDeviceData(type, device, channelsCount, channelsStart);
+       return AudioDeviceData();
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+AudioDeviceData::AudioDeviceData(DeviceType type, const m::KernelAudio::Device& device,
+    int channelsCount, int channelsStart)
+: type(type)
+, index(device.index)
+, name(device.name)
+, channelsMax(type == DeviceType::OUTPUT ? device.maxOutputChannels : device.maxInputChannels)
+, sampleRates(device.sampleRates)
+, channelsCount(channelsCount)
+, channelsStart(channelsStart)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+void AudioData::setOutputDevice(int index)
+{
+       for (AudioDeviceData& d : outputDevices)
+       {
+               if (index != d.index)
+                       continue;
+               outputDevice = d;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void AudioData::setInputDevice(int index)
+{
+       for (AudioDeviceData& d : inputDevices)
+       {
+               if (index == d.index)
+               {
+                       inputDevice = d;
+                       return;
+               }
+       }
+       inputDevice = {};
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+AudioData getAudioData()
+{
+       AudioData audioData;
+
+       audioData.apis[G_SYS_API_NONE] = "(none)";
+
+#if defined(G_OS_LINUX)
+
+       if (g_engine.kernelAudio.hasAPI(RtAudio::LINUX_ALSA))
+               audioData.apis[G_SYS_API_ALSA] = "ALSA";
+       if (g_engine.kernelAudio.hasAPI(RtAudio::UNIX_JACK))
+               audioData.apis[G_SYS_API_JACK] = "JACK";
+       if (g_engine.kernelAudio.hasAPI(RtAudio::LINUX_PULSE))
+               audioData.apis[G_SYS_API_PULSE] = "PulseAudio";
+
+#elif defined(G_OS_FREEBSD)
+
+       if (g_engine.kernelAudio.hasAPI(RtAudio::UNIX_JACK))
+               audioData.apis[G_SYS_API_JACK] = "JACK";
+       if (g_engine.kernelAudio.hasAPI(RtAudio::LINUX_PULSE))
+               audioData.apis[G_SYS_API_PULSE] = "PulseAudio";
+
+#elif defined(G_OS_WINDOWS)
+
+       if (g_engine.kernelAudio.hasAPI(RtAudio::WINDOWS_DS))
+               audioData.apis[G_SYS_API_DS] = "DirectSound";
+       if (g_engine.kernelAudio.hasAPI(RtAudio::WINDOWS_ASIO))
+               audioData.apis[G_SYS_API_ASIO] = "ASIO";
+       if (g_engine.kernelAudio.hasAPI(RtAudio::WINDOWS_WASAPI))
+               audioData.apis[G_SYS_API_WASAPI] = "WASAPI";
+
+#elif defined(G_OS_MAC)
+
+       if (g_engine.kernelAudio.hasAPI(RtAudio::MACOSX_CORE))
+               audioData.apis[G_SYS_API_CORE] = "CoreAudio";
+
+#endif
+
+       std::vector<m::KernelAudio::Device> devices = g_engine.kernelAudio.getDevices();
+
+       for (const m::KernelAudio::Device& device : devices)
+       {
+               if (device.maxOutputChannels > 0)
+                       audioData.outputDevices.push_back(AudioDeviceData(DeviceType::OUTPUT, device, G_MAX_IO_CHANS, 0));
+               if (device.maxInputChannels > 0)
+                       audioData.inputDevices.push_back(AudioDeviceData(DeviceType::INPUT, device, 1, 0));
+       }
+
+       audioData.api             = g_engine.conf.data.soundSystem;
+       audioData.bufferSize      = g_engine.conf.data.buffersize;
+       audioData.sampleRate      = g_engine.conf.data.samplerate;
+       audioData.limitOutput     = g_engine.conf.data.limitOutput;
+       audioData.recTriggerLevel = g_engine.conf.data.recTriggerLevel;
+       audioData.resampleQuality = g_engine.conf.data.rsmpQuality;
+       audioData.outputDevice    = getAudioDeviceData_(DeviceType::OUTPUT,
+        g_engine.conf.data.soundDeviceOut, g_engine.conf.data.channelsOutCount,
+        g_engine.conf.data.channelsOutStart);
+       audioData.inputDevice     = getAudioDeviceData_(DeviceType::INPUT,
+        g_engine.conf.data.soundDeviceIn, g_engine.conf.data.channelsInCount,
+        g_engine.conf.data.channelsInStart);
+
+       return audioData;
+}
+
+/* -------------------------------------------------------------------------- */
+
+MidiData getMidiData()
+{
+       MidiData midiData;
+
+#if defined(G_OS_LINUX)
+
+       if (g_engine.kernelMidi.hasAPI(RtMidi::LINUX_ALSA))
+               midiData.apis[G_MIDI_API_ALSA] = "ALSA";
+       if (g_engine.kernelMidi.hasAPI(RtMidi::UNIX_JACK))
+               midiData.apis[G_MIDI_API_JACK] = "JACK";
+
+#elif defined(G_OS_FREEBSD)
+
+       if (g_engine.kernelMidi.hasAPI(RtMidi::UNIX_JACK))
+               midiData.apis[G_MIDI_API_JACK] = "JACK";
+
+#elif defined(G_OS_WINDOWS)
+
+       if (g_engine.kernelMidi.hasAPI(RtMidi::WINDOWS_MM))
+               midiData.apis[G_MIDI_API_MM] = "Multimedia MIDI";
+
+#elif defined(G_OS_MAC)
+
+       if (g_engine.kernelMidi.hasAPI(RtMidi::MACOSX_CORE))
+               midiData.apis[G_MIDI_API_CORE] = "OSX Core MIDI";
+
+#endif
+
+       midiData.syncModes[G_MIDI_SYNC_NONE]    = "(disabled)";
+       midiData.syncModes[G_MIDI_SYNC_CLOCK_M] = "MIDI Clock (master)";
+       midiData.syncModes[G_MIDI_SYNC_MTC_M]   = "MTC (master)";
+
+       midiData.midiMaps = g_engine.midiMapper.getMapFilesFound();
+       midiData.midiMap  = u::vector::indexOf(midiData.midiMaps, g_engine.conf.data.midiMapPath);
+
+       for (unsigned i = 0; i < g_engine.kernelMidi.countOutPorts(); i++)
+               midiData.outPorts.push_back(g_engine.kernelMidi.getOutPortName(i));
+       for (unsigned i = 0; i < g_engine.kernelMidi.countInPorts(); i++)
+               midiData.inPorts.push_back(g_engine.kernelMidi.getInPortName(i));
+
+       midiData.api      = g_engine.conf.data.midiSystem;
+       midiData.syncMode = g_engine.conf.data.midiSync;
+       midiData.outPort  = g_engine.conf.data.midiPortOut;
+       midiData.inPort   = g_engine.conf.data.midiPortIn;
+
+       return midiData;
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+PluginData getPluginData()
+{
+       PluginData pluginData;
+       pluginData.numAvailablePlugins = g_engine.pluginManager.countAvailablePlugins();
+       pluginData.pluginPath          = g_engine.conf.data.pluginPath;
+       return pluginData;
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+MiscData getMiscData()
+{
+       MiscData miscData;
+       miscData.logMode      = g_engine.conf.data.logMode;
+       miscData.showTooltips = g_engine.conf.data.showTooltips;
+       return miscData;
+}
+/* -------------------------------------------------------------------------- */
+
+void save(const AudioData& data)
+{
+       g_engine.conf.data.soundSystem      = data.api;
+       g_engine.conf.data.soundDeviceOut   = data.outputDevice.index;
+       g_engine.conf.data.soundDeviceIn    = data.inputDevice.index;
+       g_engine.conf.data.channelsOutCount = data.outputDevice.channelsCount;
+       g_engine.conf.data.channelsOutStart = data.outputDevice.channelsStart;
+       g_engine.conf.data.channelsInCount  = data.inputDevice.channelsCount;
+       g_engine.conf.data.channelsInStart  = data.inputDevice.channelsStart;
+       g_engine.conf.data.limitOutput      = data.limitOutput;
+       g_engine.conf.data.rsmpQuality      = data.resampleQuality;
+       g_engine.conf.data.buffersize       = data.bufferSize;
+       g_engine.conf.data.recTriggerLevel  = data.recTriggerLevel;
+       g_engine.conf.data.samplerate       = data.sampleRate;
+       g_engine.updateMixerModel();
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void save(const PluginData& data)
+{
+       g_engine.conf.data.pluginPath = data.pluginPath;
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void save(const MidiData& data)
+{
+       g_engine.conf.data.midiSystem  = data.api;
+       g_engine.conf.data.midiPortOut = data.outPort;
+       g_engine.conf.data.midiPortIn  = data.inPort;
+       //g_engine.conf.data.midiMapPath = data.midiMap >= 0 && data.midiMap < (int)data.midiMaps.size() ? data.midiMaps[data.midiMap] : "";
+       g_engine.conf.data.midiMapPath = u::vector::atOr(data.midiMaps, data.midiMap, "");
+       g_engine.conf.data.midiSync    = data.syncMode;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void save(const MiscData& data)
+{
+       g_engine.conf.data.logMode      = data.logMode;
+       g_engine.conf.data.showTooltips = data.showTooltips;
+       Fl_Tooltip::enable(g_engine.conf.data.showTooltips);
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void scanPlugins(std::string dir, const std::function<void(float)>& progress)
+{
+       g_engine.pluginManager.scanDirs(dir, progress);
+       g_engine.pluginManager.saveList(u::fs::getHomePath() + G_SLASH + "plugins.xml");
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setPluginPathCb(void* data)
+{
+       v::gdBrowserDir* browser    = static_cast<v::gdBrowserDir*>(data);
+       std::string&     pluginPath = g_engine.conf.data.pluginPath;
+
+       if (browser->getCurrentPath() == "")
+       {
+               v::gdAlert("Invalid path.");
+               return;
+       }
+
+       if (!pluginPath.empty() && pluginPath.back() != ';')
+               pluginPath += ";";
+       pluginPath += browser->getCurrentPath();
+
+       browser->do_callback();
+
+       v::gdConfig* configWin = static_cast<v::gdConfig*>(g_ui.getSubwindow(*g_ui.mainWindow.get(), WID_CONFIG));
+       configWin->tabPlugins->rebuild();
+}
+#endif
+} // namespace giada::c::config
diff --git a/src/glue/config.h b/src/glue/config.h
new file mode 100644 (file)
index 0000000..55878c6
--- /dev/null
@@ -0,0 +1,140 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_CONFIG_H
+#define G_GLUE_CONFIG_H
+
+#include "core/kernelAudio.h"
+#include "core/types.h"
+#include <map>
+#include <string>
+#include <vector>
+
+namespace giada::c::config
+{
+enum class DeviceType
+{
+       INPUT,
+       OUTPUT
+};
+
+struct AudioDeviceData
+{
+       AudioDeviceData() = default;
+       AudioDeviceData(DeviceType t, const m::KernelAudio::Device&, int channelsCount, int channelsStart);
+
+       DeviceType       type        = DeviceType::OUTPUT;
+       int              index       = -1;
+       std::string      name        = "";
+       int              channelsMax = 0;
+       std::vector<int> sampleRates = {};
+
+       /* Selectable values. */
+
+       int channelsCount = 0;
+       int channelsStart = 0;
+};
+
+struct AudioData
+{
+       void setOutputDevice(int index);
+       void setInputDevice(int index);
+
+       std::map<int, std::string>   apis;
+       std::vector<AudioDeviceData> outputDevices;
+       std::vector<AudioDeviceData> inputDevices;
+
+       /* Selectable values. */
+
+       int             api;
+       AudioDeviceData outputDevice;
+       AudioDeviceData inputDevice;
+       int             bufferSize;
+       int             sampleRate;
+       bool            limitOutput;
+       float           recTriggerLevel;
+       int             resampleQuality;
+};
+
+struct MidiData
+{
+       std::map<int, std::string> apis;
+       std::map<int, std::string> syncModes;
+       std::vector<std::string>   midiMaps;
+       std::vector<std::string>   outPorts;
+       std::vector<std::string>   inPorts;
+
+       /* Selectable values. */
+
+       int api;
+       int syncMode;
+       int midiMap;
+       int outPort;
+       int inPort;
+};
+
+#ifdef WITH_VST
+
+struct PluginData
+{
+       int         numAvailablePlugins;
+       std::string pluginPath;
+};
+
+#endif
+
+struct MiscData
+{
+       int  logMode;
+       bool showTooltips;
+};
+
+/* get*
+Return viewModel objects filled with data. */
+
+AudioData getAudioData();
+MidiData  getMidiData();
+MiscData  getMiscData();
+#ifdef WITH_VST
+PluginData getPluginData();
+#endif
+
+void save(const AudioData&);
+void save(const MidiData&);
+void save(const MiscData&);
+#ifdef WITH_VST
+void save(const PluginData&);
+void scanPlugins(std::string dir, const std::function<void(float)>& progress);
+
+/* setPluginPathCb
+Callback attached to the DirBrowser for adding new Plug-in search paths in the
+configuration window. */
+
+void setPluginPathCb(void* data);
+#endif
+} // namespace giada::c::config
+
+#endif
diff --git a/src/glue/events.cpp b/src/glue/events.cpp
new file mode 100644 (file)
index 0000000..baa91d1
--- /dev/null
@@ -0,0 +1,306 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "events.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/engine.h"
+#include "core/eventDispatcher.h"
+#include "core/kernelAudio.h"
+#include "core/midiEvent.h"
+#include "core/mixer.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/plugins/pluginHost.h"
+#include "core/recorder.h"
+#include "core/sequencer.h"
+#include "core/types.h"
+#include "glue/main.h"
+#include "glue/plugin.h"
+#include "glue/sampleEditor.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/mainWindow/mainIO.h"
+#include "gui/elems/mainWindow/mainTimer.h"
+#include "gui/elems/sampleEditor/panTool.h"
+#include "gui/elems/sampleEditor/pitchTool.h"
+#include "gui/elems/sampleEditor/volumeTool.h"
+#include "gui/ui.h"
+#include "utils/log.h"
+#include <FL/Fl.H>
+#include <cassert>
+
+extern giada::v::Ui     g_ui;
+extern giada::m::Engine g_engine;
+
+namespace giada::c::events
+{
+namespace
+{
+void pushEvent_(m::EventDispatcher::Event e, Thread t)
+{
+       bool res = true;
+       if (t == Thread::MAIN)
+       {
+               res = g_engine.eventDispatcher.UIevents.push(e);
+       }
+       else if (t == Thread::MIDI)
+       {
+               res = g_engine.eventDispatcher.MidiEvents.push(e);
+               u::gui::ScopedLock lock;
+               g_ui.mainWindow->keyboard->notifyMidiIn(e.channelId);
+       }
+       else
+       {
+               assert(false);
+       }
+
+       if (!res)
+               G_DEBUG("[events] Queue full!\n");
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+void pressChannel(ID channelId, int velocity, Thread t)
+{
+       m::MidiEvent e;
+       e.setVelocity(velocity);
+       pushEvent_({m::EventDispatcher::EventType::KEY_PRESS, 0, channelId, velocity}, t);
+}
+
+void releaseChannel(ID channelId, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::KEY_RELEASE, 0, channelId, {}}, t);
+}
+
+void killChannel(ID channelId, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::KEY_KILL, 0, channelId, {}}, t);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setChannelVolume(ID channelId, float v, Thread t)
+{
+       v = std::clamp(v, 0.0f, G_MAX_VOLUME);
+
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_VOLUME, 0, channelId, v}, t);
+
+       sampleEditor::onRefresh(t, [v](v::gdSampleEditor& e) { e.volumeTool->update(v); });
+
+       if (t != Thread::MAIN)
+       {
+               u::gui::ScopedLock lock;
+               g_ui.mainWindow->keyboard->setChannelVolume(channelId, v);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setChannelPitch(ID channelId, float v, Thread t)
+{
+       v = std::clamp(v, G_MIN_PITCH, G_MAX_PITCH);
+
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_PITCH, 0, channelId, v}, t);
+
+       sampleEditor::onRefresh(t, [v](v::gdSampleEditor& e) { e.pitchTool->update(v); });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void sendChannelPan(ID channelId, float v)
+{
+       v = std::clamp(v, 0.0f, G_MAX_PAN);
+
+       /* Pan event is currently triggered only by the main thread. */
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_PAN, 0, channelId, v}, Thread::MAIN);
+
+       sampleEditor::onRefresh(Thread::MAIN, [v](v::gdSampleEditor& e) { e.panTool->update(v); });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toggleMuteChannel(ID channelId, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_MUTE, 0, channelId, {}}, t);
+}
+
+void toggleSoloChannel(ID channelId, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_SOLO, 0, channelId, {}}, t);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toggleArmChannel(ID channelId, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_TOGGLE_ARM, 0, channelId, {}}, t);
+}
+
+void toggleReadActionsChannel(ID channelId, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_TOGGLE_READ_ACTIONS, 0, channelId, {}}, t);
+}
+
+void killReadActionsChannel(ID channelId, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_KILL_READ_ACTIONS, 0, channelId, {}}, t);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void sendMidiToChannel(ID channelId, m::MidiEvent e, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::MIDI, 0, channelId, m::Action{0, channelId, 0, e}}, t);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toggleMetronome()
+{
+       g_engine.sequencer.toggleMetronome();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setMasterInVolume(float v, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_VOLUME, 0, m::Mixer::MASTER_IN_CHANNEL_ID, v}, t);
+
+       if (t != Thread::MAIN)
+       {
+               u::gui::ScopedLock lock;
+               g_ui.mainWindow->mainIO->setInVol(v);
+       }
+}
+
+void setMasterOutVolume(float v, Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::CHANNEL_VOLUME, 0, m::Mixer::MASTER_OUT_CHANNEL_ID, v}, t);
+
+       if (t != Thread::MAIN)
+       {
+               u::gui::ScopedLock lock;
+               g_ui.mainWindow->mainIO->setOutVol(v);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void multiplyBeats()
+{
+       main::setBeats(g_engine.sequencer.getBeats() * 2, g_engine.sequencer.getBars());
+}
+
+void divideBeats()
+{
+       main::setBeats(g_engine.sequencer.getBeats() / 2, g_engine.sequencer.getBars());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void startSequencer(Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::SEQUENCER_START, 0, 0, {}}, t);
+}
+
+void stopSequencer(Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::SEQUENCER_STOP, 0, 0, {}}, t);
+}
+
+void toggleSequencer(Thread t)
+{
+       g_engine.sequencer.isRunning() ? stopSequencer(t) : startSequencer(t);
+}
+
+void rewindSequencer(Thread t)
+{
+       pushEvent_({m::EventDispatcher::EventType::SEQUENCER_REWIND, 0, 0, {}}, t);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void stopActionRecording()
+{
+       if (g_engine.kernelAudio.isReady() && g_engine.recorder.isRecordingAction())
+               g_engine.recorder.stopActionRec(g_engine.actionRecorder);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toggleActionRecording()
+{
+       if (!g_engine.kernelAudio.isReady())
+               return;
+       if (g_engine.recorder.isRecordingAction())
+               g_engine.recorder.stopActionRec(g_engine.actionRecorder);
+       else
+               g_engine.recorder.prepareActionRec(g_engine.conf.data.recTriggerMode);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void stopInputRecording()
+{
+       if (g_engine.kernelAudio.isReady() && g_engine.recorder.isRecordingInput())
+               g_engine.recorder.stopInputRec(g_engine.conf.data.inputRecMode, g_engine.kernelAudio.getSampleRate());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toggleInputRecording()
+{
+       if (!g_engine.kernelAudio.isReady() || !g_engine.kernelAudio.isInputEnabled() || !g_engine.mixerHandler.hasInputRecordableChannels())
+               return;
+       if (g_engine.recorder.isRecordingInput())
+               g_engine.recorder.stopInputRec(g_engine.conf.data.inputRecMode, g_engine.kernelAudio.getSampleRate());
+       else
+               g_engine.recorder.prepareInputRec(g_engine.conf.data.recTriggerMode, g_engine.conf.data.inputRecMode);
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+void setPluginParameter(ID channelId, ID pluginId, int paramIndex, float value, Thread t)
+{
+       if (t == Thread::MIDI)
+       {
+               u::gui::ScopedLock lock;
+               g_ui.mainWindow->keyboard->notifyMidiIn(channelId);
+       }
+       g_engine.pluginHost.setPluginParameter(pluginId, paramIndex, value);
+       c::plugin::updateWindow(pluginId, t);
+}
+#endif
+} // namespace giada::c::events
diff --git a/src/glue/events.h b/src/glue/events.h
new file mode 100644 (file)
index 0000000..6897d98
--- /dev/null
@@ -0,0 +1,84 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_EVENTS_H
+#define G_GLUE_EVENTS_H
+
+#include "core/types.h"
+
+/* giada::c::events
+Functions that take care of live event dispatching. Every live gesture that 
+comes from the UI, MIDI thread or keyboard interaction and wants to change the
+internal engine state must call these functions. */
+
+namespace giada::m
+{
+class MidiEvent;
+}
+
+namespace giada::c::events
+{
+/* Channel*
+Channel-related events. */
+
+void pressChannel(ID channelId, int velocity, Thread t);
+void releaseChannel(ID channelId, Thread t);
+void killChannel(ID channelId, Thread t);
+void setChannelVolume(ID channelId, float v, Thread t);
+void setChannelPitch(ID channelId, float v, Thread t);
+void sendChannelPan(ID channelId, float v); // FIXME typo: should be setChannelPan
+void toggleMuteChannel(ID channelId, Thread t);
+void toggleSoloChannel(ID channelId, Thread t);
+void toggleArmChannel(ID channelId, Thread t);
+void toggleReadActionsChannel(ID channelId, Thread t);
+void killReadActionsChannel(ID channelId, Thread t);
+void sendMidiToChannel(ID channelId, m::MidiEvent e, Thread t);
+
+/* Main*
+Master I/O, transport and other engine-related events. */
+
+void toggleMetronome();
+void setMasterInVolume(float v, Thread t);
+void setMasterOutVolume(float v, Thread t);
+void multiplyBeats();
+void divideBeats();
+void startSequencer(Thread t);
+void stopSequencer(Thread t);
+void toggleSequencer(Thread t);
+void rewindSequencer(Thread t);
+void stopActionRecording();
+void toggleActionRecording();
+void stopInputRecording();
+void toggleInputRecording();
+
+/* Plug-ins. */
+
+#ifdef WITH_VST
+void setPluginParameter(ID channelId, ID pluginId, int paramIndex, float value, Thread);
+#endif
+} // namespace giada::c::events
+
+#endif
diff --git a/src/glue/io.cpp b/src/glue/io.cpp
new file mode 100644 (file)
index 0000000..30e2d4f
--- /dev/null
@@ -0,0 +1,302 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "glue/io.h"
+#include "core/conf.h"
+#include "core/engine.h"
+#include "core/kernelAudio.h"
+#include "core/midiDispatcher.h"
+#include "core/mixer.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/recorder.h"
+#include "core/wave.h"
+#include "glue/channel.h"
+#include "glue/main.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/midiIO/midiInputBase.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/mainWindow/keyboard/channelButton.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/mainWindow/keyboard/sampleChannel.h"
+#include "gui/elems/mainWindow/mainTimer.h"
+#include "gui/elems/mainWindow/mainTransport.h"
+#include "gui/ui.h"
+#include "src/core/actions/actionRecorder.h"
+#include "src/core/actions/actions.h"
+#include "utils/log.h"
+#include "utils/math.h"
+#include <FL/Fl.H>
+
+extern giada::v::Ui     g_ui;
+extern giada::m::Engine g_engine;
+
+namespace giada::c::io
+{
+namespace
+{
+void rebuildMidiWindows_()
+{
+       g_ui.rebuildSubWindow(WID_MIDI_INPUT);
+       g_ui.rebuildSubWindow(WID_MIDI_OUTPUT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool isValidKey_(int key)
+{
+       if (strlen(Fl::event_text()) == 0)
+               return false;
+       for (const auto& [_, val] : g_engine.conf.data.keyBindings)
+               if (key == val)
+                       return false;
+       return true;
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Channel_InputData::Channel_InputData(const m::Channel& c)
+: channelId(c.id)
+, channelType(c.type)
+, enabled(c.midiLearner.enabled)
+, velocityAsVol(c.samplePlayer ? c.samplePlayer->velocityAsVol : 0)
+, filter(c.midiLearner.filter)
+, keyPress(c.midiLearner.keyPress.getValue())
+, keyRelease(c.midiLearner.keyRelease.getValue())
+, kill(c.midiLearner.kill.getValue())
+, arm(c.midiLearner.arm.getValue())
+, volume(c.midiLearner.volume.getValue())
+, mute(c.midiLearner.mute.getValue())
+, solo(c.midiLearner.solo.getValue())
+, pitch(c.midiLearner.pitch.getValue())
+, readActions(c.midiLearner.readActions.getValue())
+{
+#ifdef WITH_VST
+       for (const m::Plugin* p : c.plugins)
+       {
+               PluginData pd;
+               pd.id   = p->id;
+               pd.name = p->getName();
+               for (int i = 0; i < p->getNumParameters(); i++)
+                       pd.params.push_back({i, p->getParameterName(i), p->midiInParams.at(i).getValue()});
+               plugins.push_back(pd);
+       }
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+MidiChannel_OutputData::MidiChannel_OutputData(const m::MidiSender& s)
+: enabled(s.enabled)
+, filter(s.filter)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel_OutputData::Channel_OutputData(const m::Channel& c)
+: channelId(c.id)
+, lightningEnabled(c.midiLighter.enabled)
+, lightningPlaying(c.midiLighter.playing.getValue())
+, lightningMute(c.midiLighter.mute.getValue())
+, lightningSolo(c.midiLighter.solo.getValue())
+{
+       if (c.type == ChannelType::MIDI)
+               output = std::make_optional<MidiChannel_OutputData>(*c.midiSender);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Master_InputData::Master_InputData(const m::model::MidiIn& midiIn)
+: enabled(midiIn.enabled)
+, filter(midiIn.filter)
+, rewind(midiIn.rewind)
+, startStop(midiIn.startStop)
+, actionRec(midiIn.actionRec)
+, inputRec(midiIn.inputRec)
+, volumeIn(midiIn.volumeIn)
+, volumeOut(midiIn.volumeOut)
+, beatDouble(midiIn.beatDouble)
+, beatHalf(midiIn.beatHalf)
+, metronome(midiIn.metronome)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Channel_InputData channel_getInputData(ID channelId)
+{
+       return Channel_InputData(g_engine.model.get().getChannel(channelId));
+}
+
+/* -------------------------------------------------------------------------- */
+
+Channel_OutputData channel_getOutputData(ID channelId)
+{
+       return Channel_OutputData(g_engine.model.get().getChannel(channelId));
+}
+
+/* -------------------------------------------------------------------------- */
+
+Master_InputData master_getInputData()
+{
+       return Master_InputData(g_engine.model.get().midiIn);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void channel_enableMidiLearn(ID channelId, bool v)
+{
+       g_engine.model.get().getChannel(channelId).midiLearner.enabled = v;
+       g_engine.model.swap(m::model::SwapType::NONE);
+       rebuildMidiWindows_();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void channel_enableMidiLightning(ID channelId, bool v)
+{
+       g_engine.model.get().getChannel(channelId).midiLighter.enabled = v;
+       g_engine.model.swap(m::model::SwapType::NONE);
+       rebuildMidiWindows_();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void channel_enableMidiOutput(ID channelId, bool v)
+{
+       g_engine.model.get().getChannel(channelId).midiSender->enabled = v;
+       g_engine.model.swap(m::model::SwapType::NONE);
+       rebuildMidiWindows_();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void channel_enableVelocityAsVol(ID channelId, bool v)
+{
+       g_engine.model.get().getChannel(channelId).samplePlayer->velocityAsVol = v;
+       g_engine.model.swap(m::model::SwapType::NONE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void channel_setMidiInputFilter(ID channelId, int ch)
+{
+       g_engine.model.get().getChannel(channelId).midiLearner.filter = ch;
+       g_engine.model.swap(m::model::SwapType::NONE);
+}
+
+void channel_setMidiOutputFilter(ID channelId, int ch)
+{
+       g_engine.model.get().getChannel(channelId).midiSender->filter = ch;
+       g_engine.model.swap(m::model::SwapType::NONE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool channel_setKey(ID channelId, int k)
+{
+       if (!isValidKey_(k))
+               return false;
+       g_engine.model.get().getChannel(channelId).key = k;
+       g_engine.model.swap(m::model::SwapType::HARD);
+       return true;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void channel_startMidiLearn(int param, ID channelId)
+{
+       g_engine.midiDispatcher.startChannelLearn(param, channelId, rebuildMidiWindows_);
+}
+
+void master_startMidiLearn(int param)
+{
+       g_engine.midiDispatcher.startMasterLearn(param, rebuildMidiWindows_);
+}
+
+#ifdef WITH_VST
+
+void plugin_startMidiLearn(int paramIndex, ID pluginId)
+{
+       g_engine.midiDispatcher.startPluginLearn(paramIndex, pluginId, rebuildMidiWindows_);
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void stopMidiLearn()
+{
+       g_engine.midiDispatcher.stopLearn();
+       rebuildMidiWindows_();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void channel_clearMidiLearn(int param, ID channelId)
+{
+       g_engine.midiDispatcher.clearChannelLearn(param, channelId, rebuildMidiWindows_);
+}
+
+void master_clearMidiLearn(int param)
+{
+       g_engine.midiDispatcher.clearMasterLearn(param, rebuildMidiWindows_);
+}
+
+#ifdef WITH_VST
+
+void plugin_clearMidiLearn(int param, ID pluginId)
+{
+       g_engine.midiDispatcher.clearPluginLearn(param, pluginId, rebuildMidiWindows_);
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void master_enableMidiLearn(bool v)
+{
+       g_engine.model.get().midiIn.enabled = v;
+       g_engine.model.swap(m::model::SwapType::NONE);
+       rebuildMidiWindows_();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void master_setMidiFilter(int c)
+{
+       g_engine.model.get().midiIn.filter = c;
+       g_engine.model.swap(m::model::SwapType::NONE);
+}
+} // namespace giada::c::io
diff --git a/src/glue/io.h b/src/glue/io.h
new file mode 100644 (file)
index 0000000..3a0cf0b
--- /dev/null
@@ -0,0 +1,157 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_IO_H
+#define G_GLUE_IO_H
+
+#include "core/midiEvent.h"
+#include "core/model/model.h"
+#include "core/types.h"
+
+namespace giada::m
+{
+class Channel;
+}
+
+namespace giada::c::io
+{
+struct PluginParamData
+{
+       int         index;
+       std::string name;
+       uint32_t    value;
+};
+
+struct PluginData
+{
+       ID                           id;
+       std::string                  name;
+       std::vector<PluginParamData> params;
+};
+
+struct Channel_InputData
+{
+       Channel_InputData() = default;
+       Channel_InputData(const m::Channel&);
+
+       ID          channelId;
+       ChannelType channelType;
+       bool        enabled;
+       bool        velocityAsVol;
+       int         filter;
+
+       uint32_t keyPress;
+       uint32_t keyRelease;
+       uint32_t kill;
+       uint32_t arm;
+       uint32_t volume;
+       uint32_t mute;
+       uint32_t solo;
+       uint32_t pitch;
+       uint32_t readActions;
+
+       std::vector<PluginData> plugins;
+};
+
+struct Master_InputData
+{
+       Master_InputData() = default;
+       Master_InputData(const m::model::MidiIn&);
+
+       bool enabled;
+       int  filter;
+
+       uint32_t rewind;
+       uint32_t startStop;
+       uint32_t actionRec;
+       uint32_t inputRec;
+       uint32_t volumeIn;
+       uint32_t volumeOut;
+       uint32_t beatDouble;
+       uint32_t beatHalf;
+       uint32_t metronome;
+};
+
+struct MidiChannel_OutputData
+{
+       MidiChannel_OutputData(const m::MidiSender&);
+
+       bool enabled;
+       int  filter;
+};
+
+struct Channel_OutputData
+{
+       Channel_OutputData() = default;
+       Channel_OutputData(const m::Channel&);
+
+       ID       channelId;
+       bool     lightningEnabled;
+       uint32_t lightningPlaying;
+       uint32_t lightningMute;
+       uint32_t lightningSolo;
+
+       std::optional<MidiChannel_OutputData> output;
+};
+
+Channel_InputData  channel_getInputData(ID channelId);
+Channel_OutputData channel_getOutputData(ID channelId);
+Master_InputData   master_getInputData();
+
+/* Channel functions. */
+
+void channel_enableMidiLearn(ID channelId, bool v);
+void channel_enableMidiLightning(ID channelId, bool v);
+void channel_enableMidiOutput(ID channelId, bool v);
+void channel_enableVelocityAsVol(ID channelId, bool v);
+void channel_setMidiInputFilter(ID channelId, int c);
+void channel_setMidiOutputFilter(ID channelId, int c);
+
+/* channel_setKey
+Set key 'k' to Sample Channel 'channelId'. Used for keyboard bindings. Returns
+false if the key is not valid (because used for global bindings). */
+
+bool channel_setKey(ID channelId, int k);
+
+/* MIDI Learning functions. */
+
+void channel_startMidiLearn(int param, ID channelId);
+void channel_clearMidiLearn(int param, ID channelId);
+void master_clearMidiLearn(int param);
+void master_startMidiLearn(int param);
+void stopMidiLearn();
+#ifdef WITH_VST
+void plugin_startMidiLearn(int paramIndex, ID pluginId);
+void plugin_clearMidiLearn(int param, ID pluginId);
+#endif
+
+/* Master functions. */
+
+void master_enableMidiLearn(bool v);
+void master_setMidiFilter(int c);
+} // namespace giada::c::io
+
+#endif
diff --git a/src/glue/layout.cpp b/src/glue/layout.cpp
new file mode 100644 (file)
index 0000000..76e7357
--- /dev/null
@@ -0,0 +1,253 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "glue/layout.h"
+#include "core/conf.h"
+#include "core/engine.h"
+#include "core/patch.h"
+#include "core/sequencer.h"
+#include "glue/channel.h"
+#include "glue/config.h"
+#include "glue/io.h"
+#include "glue/storage.h"
+#include "gui/dialogs/about.h"
+#include "gui/dialogs/actionEditor/midiActionEditor.h"
+#include "gui/dialogs/actionEditor/sampleActionEditor.h"
+#include "gui/dialogs/beatsInput.h"
+#include "gui/dialogs/bpmInput.h"
+#include "gui/dialogs/browser/browserDir.h"
+#include "gui/dialogs/browser/browserLoad.h"
+#include "gui/dialogs/browser/browserSave.h"
+#include "gui/dialogs/channelNameInput.h"
+#include "gui/dialogs/config.h"
+#include "gui/dialogs/keyGrabber.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/midiIO/midiInputChannel.h"
+#include "gui/dialogs/midiIO/midiInputMaster.h"
+#include "gui/dialogs/midiIO/midiOutputMidiCh.h"
+#include "gui/dialogs/midiIO/midiOutputSampleCh.h"
+#include "gui/dialogs/missingAssets.h"
+#include "gui/dialogs/pluginChooser.h"
+#include "gui/dialogs/pluginList.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/ui.h"
+
+extern giada::v::Ui     g_ui;
+extern giada::m::Engine g_engine;
+
+namespace giada::c::layout
+{
+void openBrowserForProjectLoad()
+{
+       v::gdWindow* childWin = new v::gdBrowserLoad("Open project", g_engine.conf.data.patchPath,
+           c::storage::loadProject, 0, g_engine.conf.data);
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), childWin, WID_FILE_BROWSER);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openBrowserForProjectSave()
+{
+       v::gdWindow* childWin = new v::gdBrowserSave("Save project", g_engine.conf.data.patchPath,
+           g_engine.patch.data.name, c::storage::saveProject, 0, g_engine.conf.data);
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), childWin, WID_FILE_BROWSER);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openBrowserForSampleLoad(ID channelId)
+{
+       v::gdWindow* w = new v::gdBrowserLoad("Browse sample",
+           g_engine.conf.data.samplePath.c_str(), c::storage::loadSample, channelId, g_engine.conf.data);
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), w, WID_FILE_BROWSER);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openBrowserForSampleSave(ID channelId)
+{
+       v::gdWindow* w = new v::gdBrowserSave("Save sample",
+           g_engine.conf.data.samplePath.c_str(), "", c::storage::saveSample, channelId, g_engine.conf.data);
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), w, WID_FILE_BROWSER);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openAboutWindow()
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdAbout(), WID_ABOUT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openKeyGrabberWindow(int key, std::function<bool(int)> f)
+{
+       v::gdKeyGrabber* keyGrabber = new v::gdKeyGrabber(key);
+
+       keyGrabber->onSetKey = f;
+
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), keyGrabber, WID_KEY_GRABBER);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openBpmWindow(std::string bpmValue)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdBpmInput(bpmValue.c_str()), WID_BPM);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openBeatsWindow(int beats, int bars)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdBeatsInput(beats, bars), WID_BEATS);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openConfigWindow()
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdConfig(400, 370, g_engine.conf.data), WID_CONFIG);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openMasterMidiInputWindow()
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdMidiInputMaster(g_engine.conf.data), WID_MIDI_INPUT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openChannelMidiInputWindow(ID channelId)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdMidiInputChannel(channelId, g_engine.conf.data),
+           WID_MIDI_INPUT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openSampleChannelMidiOutputWindow(ID channelId)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdMidiOutputSampleCh(channelId),
+           WID_MIDI_OUTPUT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openMidiChannelMidiOutputWindow(ID channelId)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdMidiOutputMidiCh(channelId), WID_MIDI_OUTPUT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openSampleActionEditor(ID channelId)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(),
+           new v::gdSampleActionEditor(channelId, g_engine.conf.data), WID_ACTION_EDITOR);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openMidiActionEditor(ID channelId)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(),
+           new v::gdMidiActionEditor(channelId, g_engine.conf.data), WID_ACTION_EDITOR);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openSampleEditor(ID channelId)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdSampleEditor(channelId, g_engine.conf.data),
+           WID_SAMPLE_EDITOR);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openRenameChannelWindow(const c::channel::Data& data)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdChannelNameInput(data),
+           WID_SAMPLE_NAME);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openMissingAssetsWindow(const m::LoadState& state)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdMissingAssets(state),
+           WID_MISSING_ASSETS);
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void openBrowserForPlugins(v::gdWindow& parent)
+{
+       v::gdBrowserDir* browser = new v::gdBrowserDir("Add plug-ins directory",
+           g_engine.conf.data.patchPath, c::config::setPluginPathCb, g_engine.conf.data);
+       parent.addSubWindow(browser);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openChannelPluginListWindow(ID channelId)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdPluginList(channelId, g_engine.conf.data),
+           WID_FX_LIST);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openMasterInPluginListWindow()
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdPluginList(m::Mixer::MASTER_IN_CHANNEL_ID, g_engine.conf.data),
+           WID_FX_LIST);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openMasterOutPluginListWindow()
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(), new v::gdPluginList(m::Mixer::MASTER_OUT_CHANNEL_ID, g_engine.conf.data),
+           WID_FX_LIST);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void openPluginChooser(ID channelId)
+{
+       g_ui.openSubWindow(*g_ui.mainWindow.get(),
+           new v::gdPluginChooser(g_engine.conf.data.pluginChooserX,
+               g_engine.conf.data.pluginChooserY, g_engine.conf.data.pluginChooserW,
+               g_engine.conf.data.pluginChooserH, channelId, g_engine.conf.data),
+           WID_FX_CHOOSER);
+}
+
+#endif
+} // namespace giada::c::layout
diff --git a/src/glue/layout.h b/src/glue/layout.h
new file mode 100644 (file)
index 0000000..cdc9f5a
--- /dev/null
@@ -0,0 +1,78 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_LAYOUT_H
+#define G_GLUE_LAYOUT_H
+
+#include "core/types.h"
+#include <functional>
+#include <string>
+
+namespace giada::m
+{
+struct LoadState;
+}
+
+namespace giada::v
+{
+class gdWindow;
+}
+
+namespace giada::c::channel
+{
+struct Data;
+}
+
+namespace giada::c::layout
+{
+void openBrowserForProjectLoad();
+void openBrowserForProjectSave();
+void openBrowserForSampleLoad(ID channelId);
+void openBrowserForSampleSave(ID channelId);
+void openAboutWindow();
+void openKeyGrabberWindow(int key, std::function<bool(int)>);
+void openBpmWindow(std::string bpmValue);
+void openBeatsWindow(int beats, int bars);
+void openConfigWindow();
+void openMasterMidiInputWindow();
+void openChannelMidiInputWindow(ID channelId);
+void openSampleChannelMidiOutputWindow(ID channelId);
+void openMidiChannelMidiOutputWindow(ID channelId);
+void openSampleActionEditor(ID channelId);
+void openMidiActionEditor(ID channelId);
+void openSampleEditor(ID channelId);
+void openRenameChannelWindow(const c::channel::Data&);
+void openMissingAssetsWindow(const m::LoadState&);
+#ifdef WITH_VST
+void openBrowserForPlugins(v::gdWindow& parent);
+void openChannelPluginListWindow(ID channelId);
+void openMasterInPluginListWindow();
+void openMasterOutPluginListWindow();
+void openPluginChooser(ID channelId);
+#endif
+} // namespace giada::c::layout
+
+#endif
diff --git a/src/glue/main.cpp b/src/glue/main.cpp
new file mode 100644 (file)
index 0000000..dca6339
--- /dev/null
@@ -0,0 +1,293 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "glue/main.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/engine.h"
+#include "core/init.h"
+#include "core/kernelAudio.h"
+#include "core/kernelMidi.h"
+#include "core/mixer.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/plugins/pluginHost.h"
+#include "core/plugins/pluginManager.h"
+#include "core/recorder.h"
+#include "core/sequencer.h"
+#include "core/synchronizer.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/mainWindow/keyboard/sampleChannel.h"
+#include "gui/elems/mainWindow/mainIO.h"
+#include "gui/elems/mainWindow/mainTimer.h"
+#include "gui/ui.h"
+#include "src/core/actions/actionRecorder.h"
+#include "src/core/actions/actions.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include <FL/Fl.H>
+#include <cassert>
+#include <cmath>
+
+extern giada::v::Ui     g_ui;
+extern giada::m::Engine g_engine;
+
+namespace giada::c::main
+{
+Timer::Timer(const m::model::Sequencer& c)
+: bpm(c.bpm)
+, beats(c.beats)
+, bars(c.bars)
+, quantize(c.quantize)
+, isUsingJack(g_engine.kernelAudio.getAPI() == G_SYS_API_JACK)
+, isRecordingInput(g_engine.recorder.isRecordingInput())
+{
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+IO::IO(const m::Channel& out, const m::Channel& in, const m::model::Mixer& m)
+: masterOutVol(out.volume)
+, masterInVol(in.volume)
+#ifdef WITH_VST
+, masterOutHasPlugins(out.plugins.size() > 0)
+, masterInHasPlugins(in.plugins.size() > 0)
+#endif
+, inToOut(m.inToOut)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+Peak IO::getMasterOutPeak()
+{
+       return g_engine.mixer.getPeakOut();
+}
+
+Peak IO::getMasterInPeak()
+{
+       return g_engine.mixer.getPeakIn();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool IO::isKernelReady()
+{
+       return g_engine.kernelAudio.isReady();
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Timer getTimer()
+{
+       return Timer(g_engine.model.get().sequencer);
+}
+
+/* -------------------------------------------------------------------------- */
+
+IO getIO()
+{
+       return IO(g_engine.model.get().getChannel(m::Mixer::MASTER_OUT_CHANNEL_ID),
+           g_engine.model.get().getChannel(m::Mixer::MASTER_IN_CHANNEL_ID),
+           g_engine.model.get().mixer);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Sequencer getSequencer()
+{
+       Sequencer out;
+
+       m::Mixer::RecordInfo recInfo = g_engine.mixer.getRecordInfo();
+
+       out.isFreeModeInputRec = g_engine.recorder.isRecordingInput() && g_engine.conf.data.inputRecMode == InputRecMode::FREE;
+       out.shouldBlink        = g_ui.shouldBlink() && (g_engine.sequencer.getStatus() == SeqStatus::WAITING || out.isFreeModeInputRec);
+       out.beats              = g_engine.sequencer.getBeats();
+       out.bars               = g_engine.sequencer.getBars();
+       out.currentBeat        = g_engine.sequencer.getCurrentBeat();
+       out.recPosition        = recInfo.position;
+       out.recMaxLength       = recInfo.maxLength;
+
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Transport getTransport()
+{
+       Transport transport;
+       transport.isRunning         = g_engine.sequencer.isRunning();
+       transport.isRecordingAction = g_engine.recorder.isRecordingAction();
+       transport.isRecordingInput  = g_engine.recorder.isRecordingInput();
+       transport.isMetronomeOn     = g_engine.sequencer.isMetronomeOn();
+       transport.recTriggerMode    = g_engine.conf.data.recTriggerMode;
+       transport.inputRecMode      = g_engine.conf.data.inputRecMode;
+       return transport;
+}
+
+/* -------------------------------------------------------------------------- */
+
+MainMenu getMainMenu()
+{
+       MainMenu mainMenu;
+       mainMenu.hasAudioData = g_engine.mixerHandler.hasAudioData();
+       mainMenu.hasActions   = g_engine.mixerHandler.hasActions();
+       return mainMenu;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setBpm(const char* i, const char* f)
+{
+       /* Never change this stuff while recording audio. */
+
+       if (g_engine.recorder.isRecordingInput())
+               return;
+
+       g_engine.sequencer.setBpm(std::atof(i) + (std::atof(f) / 10.0f), g_engine.kernelAudio.getSampleRate());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setBpm(float f)
+{
+       /* Never change this stuff while recording audio. */
+
+       if (g_engine.recorder.isRecordingInput())
+               return;
+
+       g_engine.sequencer.setBpm(f, g_engine.kernelAudio.getSampleRate());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setBeats(int beats, int bars)
+{
+       /* Never change this stuff while recording audio. */
+
+       if (g_engine.recorder.isRecordingInput())
+               return;
+
+       g_engine.sequencer.setBeats(beats, bars, g_engine.kernelAudio.getSampleRate());
+       g_engine.mixer.allocRecBuffer(g_engine.sequencer.getMaxFramesInLoop(g_engine.kernelAudio.getSampleRate()));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void quantize(int val)
+{
+       g_engine.sequencer.setQuantize(val, g_engine.kernelAudio.getSampleRate());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void clearAllSamples()
+{
+       if (!v::gdConfirmWin("Warning", "Free all Sample channels: are you sure?"))
+               return;
+       g_ui.closeSubWindow(WID_SAMPLE_EDITOR);
+       g_engine.sequencer.setStatus(SeqStatus::STOPPED);
+       g_engine.synchronizer.sendMIDIstop();
+       g_engine.mixerHandler.freeAllChannels();
+       g_engine.actionRecorder.clearAllActions();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void clearAllActions()
+{
+       if (!v::gdConfirmWin("Warning", "Clear all actions: are you sure?"))
+               return;
+       g_ui.closeSubWindow(WID_ACTION_EDITOR);
+       g_engine.actionRecorder.clearAllActions();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setInToOut(bool v)
+{
+       g_engine.mixerHandler.setInToOut(v);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toggleRecOnSignal()
+{
+       if (!g_engine.recorder.canEnableRecOnSignal())
+               g_engine.conf.data.recTriggerMode = RecTriggerMode::NORMAL;
+       else
+               g_engine.conf.data.recTriggerMode = g_engine.conf.data.recTriggerMode == RecTriggerMode::NORMAL ? RecTriggerMode::SIGNAL : RecTriggerMode::NORMAL;
+       g_engine.updateMixerModel();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toggleFreeInputRec()
+{
+       if (!g_engine.recorder.canEnableFreeInputRec())
+               g_engine.conf.data.inputRecMode = InputRecMode::RIGID;
+       else
+               g_engine.conf.data.inputRecMode = g_engine.conf.data.inputRecMode == InputRecMode::FREE ? InputRecMode::RIGID : InputRecMode::FREE;
+       g_engine.updateMixerModel();
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef G_DEBUG_MODE
+
+void printDebugInfo()
+{
+       g_engine.model.debug();
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void closeProject()
+{
+       if (!v::gdConfirmWin("Warning", "Close project: are you sure?"))
+               return;
+       g_engine.mixer.disable();
+       g_ui.reset();
+       g_engine.reset();
+       g_engine.mixer.enable();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void quitGiada()
+{
+       m::init::closeMainWindow();
+}
+} // namespace giada::c::main
diff --git a/src/glue/main.h b/src/glue/main.h
new file mode 100644 (file)
index 0000000..b5bf1b8
--- /dev/null
@@ -0,0 +1,146 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_MAIN_H
+#define G_MAIN_H
+
+#include "core/types.h"
+
+namespace giada::m
+{
+class Channel;
+}
+
+namespace giada::m::model
+{
+class Sequencer;
+class Mixer;
+} // namespace giada::m::model
+
+namespace giada::c::main
+{
+struct Timer
+{
+       Timer() = default;
+       Timer(const m::model::Sequencer& c);
+
+       float bpm;
+       int   beats;
+       int   bars;
+       int   quantize;
+       bool  isUsingJack;
+       bool  isRecordingInput;
+};
+
+struct IO
+{
+       IO() = default;
+       IO(const m::Channel& out, const m::Channel& in, const m::model::Mixer& m);
+
+       float masterOutVol;
+       float masterInVol;
+#ifdef WITH_VST
+       bool masterOutHasPlugins;
+       bool masterInHasPlugins;
+#endif
+       bool inToOut;
+
+       Peak getMasterOutPeak();
+       Peak getMasterInPeak();
+       bool isKernelReady();
+};
+
+struct Sequencer
+{
+       bool  isFreeModeInputRec;
+       bool  shouldBlink;
+       int   beats;
+       int   bars;
+       int   currentBeat;
+       Frame recPosition;
+       Frame recMaxLength;
+};
+
+struct Transport
+{
+       bool           isRunning;
+       bool           isRecordingAction;
+       bool           isRecordingInput;
+       bool           isMetronomeOn;
+       RecTriggerMode recTriggerMode;
+       InputRecMode   inputRecMode;
+};
+
+struct MainMenu
+{
+       bool hasAudioData;
+       bool hasActions;
+};
+
+/* get*
+Returns viewModel objects filled with data. */
+
+Timer     getTimer();
+IO        getIO();
+Sequencer getSequencer();
+Transport getTransport();
+MainMenu  getMainMenu();
+
+/* setBpm (1)
+Sets bpm value from string to float. */
+
+void setBpm(const char* v1, const char* v2);
+
+/* setBpm (2)
+Sets bpm value. Usually called from the Jack callback or non-UI components. */
+
+void setBpm(float v);
+
+void setBeats(int beats, int bars);
+void quantize(int val);
+void clearAllSamples();
+void clearAllActions();
+
+/* setInToOut
+Enables the "hear what you playing" feature. */
+
+void setInToOut(bool v);
+
+void toggleRecOnSignal();
+void toggleFreeInputRec();
+#ifdef G_DEBUG_MODE
+void printDebugInfo();
+#endif
+
+/* closeProject
+Resets Giada to init state. If resetGui also refresh all widgets. */
+
+void closeProject();
+
+void quitGiada();
+} // namespace giada::c::main
+
+#endif
diff --git a/src/glue/plugin.cpp b/src/glue/plugin.cpp
new file mode 100644 (file)
index 0000000..86d5798
--- /dev/null
@@ -0,0 +1,235 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "core/plugins/plugin.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/engine.h"
+#include "core/kernelAudio.h"
+#include "core/mixer.h"
+#include "core/model/model.h"
+#include "core/plugins/pluginHost.h"
+#include "core/plugins/pluginManager.h"
+#include "gui/dialogs/config.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/pluginList.h"
+#include "gui/dialogs/pluginWindow.h"
+#include "gui/ui.h"
+#include "plugin.h"
+#include "utils/gui.h"
+#include <FL/Fl.H>
+#include <cassert>
+#include <memory>
+
+extern giada::v::Ui     g_ui;
+extern giada::m::Engine g_engine;
+
+namespace giada::c::plugin
+{
+Param::Param(const m::Plugin& p, int index, ID channelId)
+: index(index)
+, pluginId(p.id)
+, channelId(channelId)
+, name(p.getParameterName(index))
+, text(p.getParameterText(index))
+, label(p.getParameterLabel(index))
+, value(p.getParameter(index))
+{
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Plugin::Plugin(m::Plugin& p, ID channelId)
+: id(p.id)
+, channelId(channelId)
+, valid(p.valid)
+, hasEditor(p.hasEditor())
+, isBypassed(p.isBypassed())
+, name(p.getName())
+, uniqueId(p.getUniqueId())
+, currentProgram(p.getCurrentProgram())
+, m_plugin(p)
+{
+       for (int i = 0; i < p.getNumPrograms(); i++)
+               programs.push_back({i, p.getProgramName(i)});
+       for (int i = 0; i < p.getNumParameters(); i++)
+               paramIndexes.push_back(i);
+}
+
+/* -------------------------------------------------------------------------- */
+
+juce::AudioProcessorEditor* Plugin::createEditor() const
+{
+       return m_plugin.createEditor();
+}
+
+/* -------------------------------------------------------------------------- */
+
+const m::Plugin& Plugin::getPluginRef() const { return m_plugin; }
+
+/* -------------------------------------------------------------------------- */
+
+void Plugin::setResizeCallback(std::function<void(int, int)> f)
+{
+       m_plugin.onEditorResize = f;
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Plugins::Plugins(const m::Channel& c)
+: channelId(c.id)
+, plugins(c.plugins)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Plugins getPlugins(ID channelId)
+{
+       return Plugins(g_engine.model.get().getChannel(channelId));
+}
+
+Plugin getPlugin(m::Plugin& plugin, ID channelId)
+{
+       return Plugin(plugin, channelId);
+}
+
+Param getParam(int index, const m::Plugin& plugin, ID channelId)
+{
+       return Param(plugin, index, channelId);
+}
+
+std::vector<m::PluginManager::PluginInfo> getPluginsInfo()
+{
+       return g_engine.pluginManager.getPluginsInfo();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void updateWindow(ID pluginId, Thread t)
+{
+       m::Plugin* p = g_engine.model.findShared<m::Plugin>(pluginId);
+
+       assert(p != nullptr);
+
+       if (p->hasEditor())
+               return;
+
+       /* Get the parent window first: the plug-in list. Then, if it exists, get
+    the child window - the actual pluginWindow. */
+
+       v::gdPluginList* parent = static_cast<v::gdPluginList*>(g_ui.getSubwindow(*g_ui.mainWindow.get(), WID_FX_LIST));
+       if (parent == nullptr)
+               return;
+       v::gdPluginWindow* child = static_cast<v::gdPluginWindow*>(g_ui.getSubwindow(*parent, pluginId + 1));
+       if (child == nullptr)
+               return;
+
+       if (t != Thread::MAIN)
+               u::gui::ScopedLock lock;
+       child->updateParameters(t != Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void addPlugin(int pluginListIndex, ID channelId)
+{
+       if (pluginListIndex >= g_engine.pluginManager.countAvailablePlugins())
+               return;
+       std::unique_ptr<m::Plugin> plugin    = g_engine.pluginManager.makePlugin(pluginListIndex, g_engine.kernelAudio.getSampleRate(), g_engine.kernelAudio.getBufferSize(), g_engine.sequencer);
+       const m::Plugin*           pluginPtr = plugin.get();
+       if (plugin != nullptr)
+               g_engine.pluginHost.addPlugin(std::move(plugin));
+
+       /* TODO - unfortunately JUCE wants mutable plugin objects due to the
+       presence of the non-const processBlock() method. Why not const_casting
+       only in the Plugin class? */
+       g_engine.model.get().getChannel(channelId).plugins.push_back(const_cast<m::Plugin*>(pluginPtr));
+       g_engine.model.swap(m::model::SwapType::HARD);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void swapPlugins(const m::Plugin& p1, const m::Plugin& p2, ID channelId)
+{
+       g_engine.pluginHost.swapPlugin(p1, p2, g_engine.model.get().getChannel(channelId).plugins);
+       g_engine.model.swap(m::model::SwapType::HARD);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void sortPlugins(m::PluginManager::SortMethod method)
+{
+       g_engine.pluginManager.sortPlugins(method);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void freePlugin(const m::Plugin& plugin, ID channelId)
+{
+       u::vector::remove(g_engine.model.get().getChannel(channelId).plugins, &plugin);
+       g_engine.model.swap(m::model::SwapType::HARD);
+
+       g_engine.pluginHost.freePlugin(plugin);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setProgram(ID pluginId, int programIndex)
+{
+       g_engine.pluginHost.setPluginProgram(pluginId, programIndex);
+       updateWindow(pluginId, Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toggleBypass(ID pluginId)
+{
+       g_engine.pluginHost.toggleBypass(pluginId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void startDispatchLoop()
+{
+       g_ui.startJuceDispatchLoop();
+}
+
+void stopDispatchLoop()
+{
+       g_ui.stopJuceDispatchLoop();
+}
+} // namespace giada::c::plugin
+
+#endif
diff --git a/src/glue/plugin.h b/src/glue/plugin.h
new file mode 100644 (file)
index 0000000..aaebab7
--- /dev/null
@@ -0,0 +1,132 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_PLUGIN_H
+#define G_GLUE_PLUGIN_H
+
+#ifdef WITH_VST
+
+#include "core/plugins/pluginHost.h"
+#include "core/plugins/pluginManager.h"
+#include "core/types.h"
+#include <string>
+#include <vector>
+
+namespace juce
+{
+class AudioProcessorEditor;
+}
+
+namespace giada::m
+{
+class Plugin;
+class Channel;
+} // namespace giada::m
+
+namespace giada::c::plugin
+{
+struct Program
+{
+       int         index;
+       std::string name;
+};
+
+struct Param
+{
+       Param() = default;
+       Param(const m::Plugin&, int index, ID channelId);
+
+       int         index;
+       ID          pluginId;
+       ID          channelId;
+       std::string name;
+       std::string text;
+       std::string label;
+       float       value;
+};
+
+struct Plugin
+{
+       Plugin(m::Plugin&, ID channelId);
+
+       juce::AudioProcessorEditor* createEditor() const;
+       const m::Plugin&            getPluginRef() const;
+
+       void setResizeCallback(std::function<void(int, int)> f);
+
+       ID          id;
+       ID          channelId;
+       bool        valid;
+       bool        hasEditor;
+       bool        isBypassed;
+       std::string name;
+       std::string uniqueId;
+       int         currentProgram;
+
+       std::vector<Program> programs;
+       std::vector<int>     paramIndexes;
+
+private:
+       m::Plugin& m_plugin;
+};
+
+struct Plugins
+{
+       Plugins() = default;
+       Plugins(const m::Channel&);
+
+       ID                      channelId;
+       std::vector<m::Plugin*> plugins;
+};
+
+/* get*
+Returns ViewModel objects. */
+
+Plugins getPlugins(ID channelId);
+Plugin  getPlugin(m::Plugin& plugin, ID channelId);
+Param   getParam(int index, const m::Plugin& plugin, ID channelId);
+
+std::vector<m::PluginManager::PluginInfo> getPluginsInfo();
+
+/* updateWindow
+Updates the editor-less plug-in window. This is useless if the plug-in has an
+editor. */
+
+void updateWindow(ID pluginId, Thread);
+
+void addPlugin(int pluginListIndex, ID channelId);
+void swapPlugins(const m::Plugin& p1, const m::Plugin& p2, ID channelId);
+void sortPlugins(m::PluginManager::SortMethod);
+void freePlugin(const m::Plugin& plugin, ID channelId);
+void setProgram(ID pluginId, int programIndex);
+void toggleBypass(ID pluginId);
+void startDispatchLoop();
+void stopDispatchLoop();
+} // namespace giada::c::plugin
+
+#endif
+
+#endif
diff --git a/src/glue/recorder.cpp b/src/glue/recorder.cpp
new file mode 100644 (file)
index 0000000..a25f382
--- /dev/null
@@ -0,0 +1,92 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "glue/recorder.h"
+#include "core/actions/actionRecorder.h"
+#include "core/channels/channel.h"
+#include "core/const.h"
+#include "core/engine.h"
+#include "core/kernelMidi.h"
+#include "core/mixer.h"
+#include "core/model/model.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/mainWindow/keyboard/sampleChannel.h"
+#include "gui/ui.h"
+#include "src/core/actions/action.h"
+#include "src/core/actions/actionRecorder.h"
+#include "src/core/actions/actions.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include <cassert>
+
+extern giada::m::Engine g_engine;
+extern giada::v::Ui     g_ui;
+
+namespace giada::c::recorder
+{
+void clearAllActions(ID channelId)
+{
+       if (!v::gdConfirmWin("Warning", "Clear all actions: are you sure?"))
+               return;
+       g_engine.actionRecorder.clearChannel(channelId);
+       updateChannel(channelId, /*updateActionEditor=*/true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void clearVolumeActions(ID channelId)
+{
+       if (!v::gdConfirmWin("Warning", "Clear all volume actions: are you sure?"))
+               return;
+       g_engine.actionRecorder.clearActions(channelId, m::MidiEvent::ENVELOPE);
+       updateChannel(channelId, /*updateActionEditor=*/true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void clearStartStopActions(ID channelId)
+{
+       if (!v::gdConfirmWin("Warning", "Clear all start/stop actions: are you sure?"))
+               return;
+       g_engine.actionRecorder.clearActions(channelId, m::MidiEvent::NOTE_ON);
+       g_engine.actionRecorder.clearActions(channelId, m::MidiEvent::NOTE_OFF);
+       g_engine.actionRecorder.clearActions(channelId, m::MidiEvent::NOTE_KILL);
+       updateChannel(channelId, /*updateActionEditor=*/true);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void updateChannel(ID channelId, bool updateActionEditor)
+{
+       /* TODO - move somewhere else in the core area */
+       g_engine.model.get().getChannel(channelId).hasActions = g_engine.actionRecorder.hasActions(channelId);
+       g_engine.model.swap(m::model::SwapType::HARD);
+
+       if (updateActionEditor)
+               g_ui.refreshSubWindow(WID_ACTION_EDITOR);
+}
+} // namespace giada::c::recorder
diff --git a/src/glue/recorder.h b/src/glue/recorder.h
new file mode 100644 (file)
index 0000000..efd247e
--- /dev/null
@@ -0,0 +1,40 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_RECORDER_H
+#define G_GLUE_RECORDER_H
+
+#include "core/types.h"
+
+namespace giada::c::recorder
+{
+void clearAllActions(ID channelId);
+void clearVolumeActions(ID channelId);
+void clearStartStopActions(ID channelId);
+void updateChannel(ID channelId, bool updateActionEditor);
+} // namespace giada::c::recorder
+
+#endif
diff --git a/src/glue/sampleEditor.cpp b/src/glue/sampleEditor.cpp
new file mode 100644 (file)
index 0000000..942fb9e
--- /dev/null
@@ -0,0 +1,400 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/sampleEditor.h"
+#include "channel.h"
+#include "core/const.h"
+#include "core/engine.h"
+#include "core/kernelAudio.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/sequencer.h"
+#include "core/wave.h"
+#include "core/waveManager.h"
+#include "glue/events.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/sampleEditor/boostTool.h"
+#include "gui/elems/sampleEditor/panTool.h"
+#include "gui/elems/sampleEditor/pitchTool.h"
+#include "gui/elems/sampleEditor/rangeTool.h"
+#include "gui/elems/sampleEditor/shiftTool.h"
+#include "gui/elems/sampleEditor/volumeTool.h"
+#include "gui/elems/sampleEditor/waveTools.h"
+#include "gui/elems/sampleEditor/waveform.h"
+#include "gui/ui.h"
+#include "sampleEditor.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include <FL/Fl.H>
+#include <cassert>
+#include <memory>
+
+extern giada::v::Ui     g_ui;
+extern giada::m::Engine g_engine;
+
+namespace giada::c::sampleEditor
+{
+namespace
+{
+m::Channel& getChannel_(ID channelId)
+{
+       return g_engine.model.get().getChannel(channelId);
+}
+
+m::SamplePlayer& getSamplePlayer_(ID channelId)
+{
+       return getChannel_(channelId).samplePlayer.value();
+}
+
+m::Wave& getWave_(ID channelId)
+{
+       return *const_cast<m::Wave*>(getSamplePlayer_(channelId).getWave());
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* waveBuffer
+A Wave used during cut/copy/paste operations. */
+
+std::unique_ptr<m::Wave> waveBuffer_;
+
+Frame previewTracker_ = 0;
+
+/* -------------------------------------------------------------------------- */
+
+/* resetBeginEnd_
+Resets begin/end points to 0/max. */
+
+void resetBeginEnd_(ID channelId)
+{
+       Frame begin = getSamplePlayer_(channelId).begin;
+       Frame end   = getSamplePlayer_(channelId).getWaveSize();
+       setBeginEnd(channelId, begin, end);
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Data::Data(const m::Channel& c)
+: channelId(c.id)
+, name(c.name)
+, volume(c.volume)
+, pan(c.pan)
+, pitch(c.samplePlayer->pitch)
+, begin(c.samplePlayer->begin)
+, end(c.samplePlayer->end)
+, shift(c.samplePlayer->shift)
+, waveSize(c.samplePlayer->getWave()->getBuffer().countFrames())
+, waveBits(c.samplePlayer->getWave()->getBits())
+, waveDuration(c.samplePlayer->getWave()->getDuration())
+, waveRate(c.samplePlayer->getWave()->getRate())
+, wavePath(c.samplePlayer->getWave()->getPath())
+, isLogical(c.samplePlayer->getWave()->isLogical())
+, m_channel(&c)
+{
+}
+
+ChannelStatus Data::a_getPreviewStatus() const
+{
+       return getChannel_(m::Mixer::PREVIEW_CHANNEL_ID).shared->playStatus.load();
+}
+
+Frame Data::a_getPreviewTracker() const
+{
+       return getChannel_(m::Mixer::PREVIEW_CHANNEL_ID).shared->tracker.load();
+}
+
+const m::Wave& Data::getWaveRef() const
+{
+       return *m_channel->samplePlayer->getWave();
+}
+
+Frame Data::getFramesInBar() const
+{
+       return g_engine.sequencer.getFramesInBar();
+}
+
+Frame Data::getFramesInLoop() const
+{
+       return g_engine.sequencer.getFramesInLoop();
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+Data getData(ID channelId)
+{
+       /* Prepare the preview channel first, then return Data object. */
+       m::Channel& previewChannel = getChannel_(m::Mixer::PREVIEW_CHANNEL_ID);
+       previewChannel.samplePlayer->loadWave(*previewChannel.shared, &getWave_(channelId));
+       g_engine.model.swap(m::model::SwapType::SOFT);
+
+       return Data(getChannel_(channelId));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void onRefresh(Thread t, std::function<void(v::gdSampleEditor&)> f)
+{
+       v::gdSampleEditor* se = static_cast<v::gdSampleEditor*>(g_ui.getSubwindow(*g_ui.mainWindow.get(), WID_SAMPLE_EDITOR));
+       if (se == nullptr)
+               return;
+       if (t != Thread::MAIN)
+               u::gui::ScopedLock lock;
+       f(*se);
+}
+
+v::gdSampleEditor* getSampleEditorWindow()
+{
+       v::gdSampleEditor* se = static_cast<v::gdSampleEditor*>(g_ui.getSubwindow(*g_ui.mainWindow.get(), WID_SAMPLE_EDITOR));
+       assert(se != nullptr);
+       return se;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void setBeginEnd(ID channelId, Frame b, Frame e)
+{
+       m::Channel& c = getChannel_(channelId);
+
+       b = std::clamp(b, 0, c.samplePlayer->getWaveSize() - 1);
+       e = std::clamp(e, 1, c.samplePlayer->getWaveSize() - 1);
+       if (b >= e)
+               b = e - 1;
+       else if (e < b)
+               e = b + 1;
+
+       if (c.shared->tracker.load() < b)
+               c.shared->tracker.store(b);
+
+       getSamplePlayer_(channelId).begin = b;
+       getSamplePlayer_(channelId).end   = e;
+       g_engine.model.swap(m::model::SwapType::SOFT);
+
+       /* TODO waveform widget is dumb and wants a rebuild. Refactoring needed! */
+       getSampleEditorWindow()->rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void cut(ID channelId, Frame a, Frame b)
+{
+       copy(channelId, a, b);
+       m::model::DataLock lock = g_engine.model.lockData();
+       m::wfx::cut(getWave_(channelId), a, b);
+       resetBeginEnd_(channelId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void copy(ID channelId, Frame a, Frame b)
+{
+       waveBuffer_ = g_engine.waveManager.createFromWave(getWave_(channelId), a, b);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void paste(ID channelId, Frame a)
+{
+       if (!isWaveBufferFull())
+       {
+               u::log::print("[sampleEditor::paste] Buffer is empty, nothing to paste\n");
+               return;
+       }
+
+       /* Get the existing wave in channel. */
+
+       m::Wave& wave = getWave_(channelId);
+
+       /* Temporary disable wave reading in channel. From now on, the audio thread
+       won't be reading any wave, so editing it is safe.  */
+
+       m::model::DataLock lock = g_engine.model.lockData();
+
+       /* Paste copied data to destination wave. */
+
+       m::wfx::paste(*waveBuffer_, wave, a);
+
+       /* Pass the old wave that contains the pasted data to channel. */
+
+       getChannel_(channelId).samplePlayer->setWave(&wave, 1.0f);
+
+       /* In the meantime, shift begin/end points to keep the previous position. */
+
+       int   delta = waveBuffer_->getBuffer().countFrames();
+       Frame begin = getSamplePlayer_(channelId).begin;
+       Frame end   = getSamplePlayer_(channelId).end;
+
+       if (a < begin && a < end)
+               setBeginEnd(channelId, begin + delta, end + delta);
+       else if (a < end)
+               setBeginEnd(channelId, begin, end + delta);
+
+       getSampleEditorWindow()->rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void silence(ID channelId, int a, int b)
+{
+       m::model::DataLock lock = g_engine.model.lockData();
+       m::wfx::silence(getWave_(channelId), a, b);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void fade(ID channelId, int a, int b, m::wfx::Fade type)
+{
+       m::model::DataLock lock = g_engine.model.lockData();
+       m::wfx::fade(getWave_(channelId), a, b, type);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void smoothEdges(ID channelId, int a, int b)
+{
+       m::model::DataLock lock = g_engine.model.lockData();
+       m::wfx::smooth(getWave_(channelId), a, b);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void reverse(ID channelId, Frame a, Frame b)
+{
+       m::model::DataLock lock = g_engine.model.lockData();
+       m::wfx::reverse(getWave_(channelId), a, b);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void normalize(ID channelId, int a, int b)
+{
+       m::model::DataLock lock = g_engine.model.lockData();
+       m::wfx::normalize(getWave_(channelId), a, b);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void trim(ID channelId, int a, int b)
+{
+       m::model::DataLock lock = g_engine.model.lockData();
+       m::wfx::trim(getWave_(channelId), a, b);
+       resetBeginEnd_(channelId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* TODO - this arcane logic of keeping previewTracker_ will go away as soon as
+the One-shot pause mode is implemented: 
+       https://github.com/monocasual/giada/issues/88 */
+
+void playPreview(bool loop)
+{
+       setPreviewTracker(previewTracker_);
+       channel::setSamplePlayerMode(m::Mixer::PREVIEW_CHANNEL_ID, loop ? SamplePlayerMode::SINGLE_ENDLESS : SamplePlayerMode::SINGLE_BASIC);
+       events::pressChannel(m::Mixer::PREVIEW_CHANNEL_ID, G_MAX_VELOCITY, Thread::MAIN);
+}
+
+void stopPreview()
+{
+       /* Let the Sample Editor show the initial tracker position, then kill the
+       channel. */
+       setPreviewTracker(previewTracker_);
+       getSampleEditorWindow()->refresh();
+       events::killChannel(m::Mixer::PREVIEW_CHANNEL_ID, Thread::MAIN);
+}
+
+void setPreviewTracker(Frame f)
+{
+       g_engine.model.get().getChannel(m::Mixer::PREVIEW_CHANNEL_ID).shared->tracker.store(f);
+       g_engine.model.swap(m::model::SwapType::SOFT);
+
+       previewTracker_ = f;
+
+       getSampleEditorWindow()->refresh();
+}
+
+void cleanupPreview()
+{
+       m::Channel& channel = getChannel_(m::Mixer::PREVIEW_CHANNEL_ID);
+
+       channel.samplePlayer->loadWave(*channel.shared, nullptr);
+       g_engine.model.swap(m::model::SwapType::SOFT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void toNewChannel(ID channelId, Frame a, Frame b)
+{
+       ID columnId = g_ui.mainWindow->keyboard->getChannelColumnId(channelId);
+       g_engine.mixerHandler.addAndLoadChannel(columnId, g_engine.waveManager.createFromWave(getWave_(channelId), a, b),
+           g_engine.kernelAudio.getBufferSize(), g_engine.channelManager);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool isWaveBufferFull()
+{
+       return waveBuffer_ != nullptr;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void reload(ID channelId)
+{
+       if (!v::gdConfirmWin("Warning", "Reload sample: are you sure?"))
+               return;
+
+       if (channel::loadChannel(channelId, getWave_(channelId).getPath()) != G_RES_OK)
+       {
+               v::gdAlert("Unable to reload sample!");
+               return;
+       }
+
+       getSampleEditorWindow()->rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void shift(ID channelId, Frame offset)
+{
+       Frame shift = getSamplePlayer_(channelId).shift;
+
+       m::model::DataLock lock = g_engine.model.lockData();
+
+       m::wfx::shift(getWave_(channelId), offset - shift);
+       getSamplePlayer_(channelId).shift = offset;
+
+       getSampleEditorWindow()->shiftTool->update(offset);
+}
+} // namespace giada::c::sampleEditor
diff --git a/src/glue/sampleEditor.h b/src/glue/sampleEditor.h
new file mode 100644 (file)
index 0000000..780c63b
--- /dev/null
@@ -0,0 +1,118 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_SAMPLE_EDITOR_H
+#define G_GLUE_SAMPLE_EDITOR_H
+
+#include "core/types.h"
+#include "core/waveFx.h"
+#include <functional>
+#include <string>
+
+namespace giada::m
+{
+class Wave;
+class Channel;
+} // namespace giada::m
+
+namespace giada::v
+{
+class gdSampleEditor;
+}
+
+namespace giada::c::sampleEditor
+{
+struct Data
+{
+       Data() = default;
+       Data(const m::Channel&);
+
+       ChannelStatus  a_getPreviewStatus() const;
+       Frame          a_getPreviewTracker() const;
+       const m::Wave& getWaveRef() const; // TODO - getWaveData (or public ptr member to Wave::data)
+       Frame          getFramesInBar() const;
+       Frame          getFramesInLoop() const;
+
+       ID          channelId;
+       std::string name;
+       float       volume;
+       float       pan;
+       float       pitch;
+       Frame       begin;
+       Frame       end;
+       Frame       shift;
+       Frame       waveSize;
+       int         waveBits;
+       int         waveDuration;
+       int         waveRate;
+       std::string wavePath;
+       bool        isLogical;
+
+private:
+       const m::Channel* m_channel;
+};
+
+/* onRefresh --- TODO - wrong name */
+
+void onRefresh(Thread, std::function<void(v::gdSampleEditor&)> f);
+
+/* getData
+Returns a Data object filled with data from a channel. */
+
+Data getData(ID channelId);
+
+/* setBeginEnd
+Sets start/end points in the sample editor. */
+
+void setBeginEnd(ID channelId, Frame b, Frame e);
+
+void cut(ID channelId, Frame a, Frame b);
+void copy(ID channelId, Frame a, Frame b);
+void paste(ID channelId, Frame a);
+
+void trim(ID channelId, Frame a, Frame b);
+void reverse(ID channelId, Frame a, Frame b);
+void normalize(ID channelId, Frame a, Frame b);
+void silence(ID channelId, Frame a, Frame b);
+void fade(ID channelId, Frame a, Frame b, m::wfx::Fade type);
+void smoothEdges(ID channelId, Frame a, Frame b);
+void shift(ID channelId, Frame offset);
+void reload(ID channelId);
+
+bool isWaveBufferFull();
+
+void playPreview(bool loop);
+void stopPreview();
+void setPreviewTracker(Frame f);
+void cleanupPreview();
+
+/* toNewChannel
+Copies the selected range into a new sample channel. */
+
+void toNewChannel(ID channelId, Frame a, Frame b);
+} // namespace giada::c::sampleEditor
+
+#endif
diff --git a/src/glue/storage.cpp b/src/glue/storage.cpp
new file mode 100644 (file)
index 0000000..8a7ab66
--- /dev/null
@@ -0,0 +1,207 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/model/storage.h"
+#include "channel.h"
+#include "core/conf.h"
+#include "core/engine.h"
+#include "core/init.h"
+#include "core/mixer.h"
+#include "core/mixerHandler.h"
+#include "core/model/model.h"
+#include "core/patch.h"
+#include "core/plugins/plugin.h"
+#include "core/plugins/pluginHost.h"
+#include "core/plugins/pluginManager.h"
+#include "core/sequencer.h"
+#include "core/wave.h"
+#include "core/waveManager.h"
+#include "glue/layout.h"
+#include "glue/main.h"
+#include "gui/dialogs/browser/browserLoad.h"
+#include "gui/dialogs/browser/browserSave.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/basics/progress.h"
+#include "gui/elems/mainWindow/keyboard/column.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/ui.h"
+#include "src/core/actions/actionRecorder.h"
+#include "storage.h"
+#include "utils/fs.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include <cassert>
+
+extern giada::m::Engine g_engine;
+extern giada::v::Ui     g_ui;
+
+namespace giada::c::storage
+{
+void loadProject(void* data)
+{
+       v::gdBrowserLoad* browser = static_cast<v::gdBrowserLoad*>(data);
+
+       const std::string projectPath = browser->getSelectedItem();
+       const std::string patchPath   = projectPath + G_SLASH + u::fs::stripExt(u::fs::basename(projectPath)) + ".gptc";
+
+       auto progress   = g_ui.mainWindow->getScopedProgress("Loading project...");
+       auto progressCb = [&p = progress.get()](float v) {
+               p.setProgress(v);
+       };
+
+       /* Close all sub-windows first, in case there are VST editors visible. VST
+       editors must be closed before deleting their plug-in processors. */
+
+       g_ui.closeAllSubwindows();
+
+       m::LoadState state = g_engine.load(projectPath, patchPath, progressCb);
+
+       if (state.patch != G_PATCH_OK)
+       {
+               if (state.patch == G_PATCH_UNREADABLE)
+                       v::gdAlert("This patch is unreadable.");
+               else if (state.patch == G_PATCH_INVALID)
+                       v::gdAlert("This patch is not valid.");
+               else if (state.patch == G_PATCH_UNSUPPORTED)
+                       v::gdAlert("This patch format is no longer supported.");
+               return;
+       }
+
+       /* Update UI. */
+
+       g_ui.load(g_engine.patch.data);
+
+       if (!state.isGood())
+               layout::openMissingAssetsWindow(state);
+
+       browser->do_callback();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void saveProject(void* data)
+{
+       v::gdBrowserSave* browser = static_cast<v::gdBrowserSave*>(data);
+
+       const std::string projectName = u::fs::stripExt(browser->getName());
+       const std::string projectPath = browser->getCurrentPath() + G_SLASH + projectName + ".gprj";
+       const std::string patchPath   = projectPath + G_SLASH + projectName + ".gptc";
+
+       if (projectName == "")
+       {
+               v::gdAlert("Please choose a project name.");
+               return;
+       }
+
+       if (u::fs::dirExists(projectPath) && !v::gdConfirmWin("Warning", "Project exists: overwrite?"))
+               return;
+
+       auto progress   = g_ui.mainWindow->getScopedProgress("Saving project...");
+       auto progressCb = [&p = progress.get()](float v) {
+               p.setProgress(v);
+       };
+
+       g_ui.store(projectName, g_engine.patch.data);
+
+       if (!g_engine.store(projectName, projectPath, patchPath, progressCb))
+       {
+               v::gdAlert("Unable to save the project!");
+               return;
+       }
+
+       browser->do_callback();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void loadSample(void* data)
+{
+       v::gdBrowserLoad* browser  = static_cast<v::gdBrowserLoad*>(data);
+       std::string       fullPath = browser->getSelectedItem();
+
+       if (fullPath.empty())
+               return;
+
+       auto progress = g_ui.mainWindow->getScopedProgress("Loading sample...");
+
+       if (int res = c::channel::loadChannel(browser->getChannelId(), fullPath); res == G_RES_OK)
+       {
+               g_engine.conf.data.samplePath = u::fs::dirname(fullPath);
+               browser->do_callback();
+               g_ui.mainWindow->delSubWindow(WID_SAMPLE_EDITOR); // if editor is open
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void saveSample(void* data)
+{
+       v::gdBrowserSave* browser    = static_cast<v::gdBrowserSave*>(data);
+       std::string       name       = browser->getName();
+       std::string       folderPath = browser->getCurrentPath();
+       ID                channelId  = browser->getChannelId();
+
+       if (name == "")
+       {
+               v::gdAlert("Please choose a file name.");
+               return;
+       }
+
+       std::string filePath = folderPath + G_SLASH + u::fs::stripExt(name) + ".wav";
+
+       if (u::fs::fileExists(filePath) && !v::gdConfirmWin("Warning", "File exists: overwrite?"))
+               return;
+
+       ID       waveId = g_engine.model.get().getChannel(channelId).samplePlayer->getWaveId();
+       m::Wave* wave   = g_engine.model.findShared<m::Wave>(waveId);
+
+       assert(wave != nullptr);
+
+       if (!g_engine.waveManager.save(*wave, filePath))
+       {
+               v::gdAlert("Unable to save this sample!");
+               return;
+       }
+
+       u::log::print("[saveSample] sample saved to %s\n", filePath);
+
+       /* Update last used path in conf, so that it can be reused next time. */
+
+       g_engine.conf.data.samplePath = u::fs::dirname(filePath);
+
+       /* Update logical and edited states in Wave. */
+
+       m::model::DataLock lock = g_engine.model.lockData();
+       wave->setLogical(false);
+       wave->setEdited(false);
+
+       /* Finally close the browser. */
+
+       browser->do_callback();
+}
+} // namespace giada::c::storage
\ No newline at end of file
diff --git a/src/glue/storage.h b/src/glue/storage.h
new file mode 100644 (file)
index 0000000..b6df6dd
--- /dev/null
@@ -0,0 +1,38 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_GLUE_STORAGE_H
+#define G_GLUE_STORAGE_H
+
+namespace giada::c::storage
+{
+void loadProject(void* data);
+void saveProject(void* data);
+void saveSample(void* data);
+void loadSample(void* data);
+} // namespace giada::c::storage
+
+#endif
diff --git a/src/gui/dialogs/about.cpp b/src/gui/dialogs/about.cpp
new file mode 100644 (file)
index 0000000..ac3ded7
--- /dev/null
@@ -0,0 +1,109 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include <FL/Fl_Pixmap.H>
+#include <FL/fl_draw.H>
+#ifdef WITH_VST
+#include "deps/juce-config.h"
+#endif
+#include "about.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+
+namespace giada::v
+{
+gdAbout::gdAbout()
+#ifdef WITH_VST
+: gdWindow(340, 415, "About Giada")
+#else
+: gdWindow(340, 330, "About Giada")
+#endif
+, logo(8, 20, 324, 86)
+, text(8, 120, 324, 140)
+, close(252, h() - 28, 80, 20, "Close")
+#ifdef WITH_VST
+, vstText(8, 315, 324, 46)
+, vstLogo(8, 265, 324, 50)
+#endif
+{
+       set_modal();
+
+       std::string version = G_VERSION_STR;
+#ifdef G_DEBUG_MODE
+       version += " (debug build)";
+#endif
+
+       logo.image(new Fl_Pixmap(giada_logo_xpm));
+       text.align(FL_ALIGN_CENTER | FL_ALIGN_INSIDE | FL_ALIGN_TOP);
+       text.copy_label(std::string(
+           "Version " + version + " (" BUILD_DATE ")\n\n"
+                                  "Developed by Monocasual Laboratories\n\n"
+                                  "Released under the terms of the GNU General\n"
+                                  "Public License (GPL v3)\n\n"
+                                  "News, infos, contacts and documentation:\n"
+                                  "www.giadamusic.com")
+                           .c_str());
+
+       add(logo);
+       add(text);
+       add(close);
+
+#ifdef WITH_VST
+
+       vstLogo.image(new Fl_Pixmap(vstLogo_xpm));
+       vstLogo.position(vstLogo.x(), text.y() + text.h() + 8);
+       vstText.label(
+           "VST Plug-In Technology by Steinberg\n"
+           "VST is a trademark of Steinberg\nMedia Technologies GmbH");
+       vstText.position(vstText.x(), vstLogo.y() + vstLogo.h());
+
+       add(vstLogo);
+       add(vstText);
+
+#endif
+
+       close.callback(cb_close, (void*)this);
+       u::gui::setFavicon(this);
+       setId(WID_ABOUT);
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdAbout::cb_close(Fl_Widget* /*w*/, void* p) { ((gdAbout*)p)->cb_close(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdAbout::cb_close()
+{
+       do_callback();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/about.h b/src/gui/dialogs/about.h
new file mode 100644 (file)
index 0000000..7083d6d
--- /dev/null
@@ -0,0 +1,55 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_ABOUT_H
+#define GD_ABOUT_H
+
+#include "gui/dialogs/window.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+
+namespace giada::v
+{
+class gdAbout : public gdWindow
+{
+public:
+       gdAbout();
+
+       static void cb_close(Fl_Widget* /*w*/, void* p);
+       inline void cb_close();
+
+private:
+       geBox    logo;
+       geBox    text;
+       geButton close;
+#ifdef WITH_VST
+       geBox vstText;
+       geBox vstLogo;
+#endif
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/actionEditor/baseActionEditor.cpp b/src/gui/dialogs/actionEditor/baseActionEditor.cpp
new file mode 100644 (file)
index 0000000..18c9051
--- /dev/null
@@ -0,0 +1,229 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/actionEditor/baseActionEditor.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "glue/channel.h"
+#include "gui/drawing.h"
+#include "gui/elems/actionEditor/gridTool.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/choice.h"
+#include "gui/elems/basics/scrollPack.h"
+#include "src/core/actions/action.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <FL/Fl.H>
+#include <FL/fl_draw.H>
+#include <cassert>
+#include <limits>
+#include <string>
+
+namespace giada::v
+{
+gdBaseActionEditor::gdBaseActionEditor(ID channelId, m::Conf::Data& conf)
+: gdWindow(conf.actionEditorX, conf.actionEditorY, conf.actionEditorW, conf.actionEditorH)
+, channelId(channelId)
+, gridTool(0, 0, conf)
+, zoomInBtn(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", zoomInOff_xpm, zoomInOn_xpm)
+, zoomOutBtn(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", zoomOutOff_xpm, zoomOutOn_xpm)
+, m_barTop(0, 0, Direction::HORIZONTAL)
+, m_splitScroll(0, 0, 0, 0)
+, m_conf(conf)
+, m_ratio(conf.actionEditorZoom)
+{
+       end();
+
+       m_barTop.position(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN);
+
+       m_splitScroll.resize(
+           G_GUI_OUTER_MARGIN,
+           (G_GUI_OUTER_MARGIN * 2) + 20,
+           w() - G_GUI_OUTER_MARGIN * 2,
+           h() - (G_GUI_OUTER_MARGIN * 3) - 20);
+
+       zoomInBtn.callback(cb_zoomIn, this);
+       zoomInBtn.copy_tooltip("Zoom in");
+       zoomOutBtn.callback(cb_zoomOut, this);
+       zoomOutBtn.copy_tooltip("Zoom out");
+
+       add(m_barTop);
+       add(m_splitScroll);
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdBaseActionEditor::~gdBaseActionEditor()
+{
+       using namespace giada::m;
+
+       m_conf.actionEditorX      = x();
+       m_conf.actionEditorY      = y();
+       m_conf.actionEditorW      = w();
+       m_conf.actionEditorH      = h();
+       m_conf.actionEditorSplitH = m_splitScroll.getTopContentH();
+       m_conf.actionEditorZoom   = m_ratio;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int gdBaseActionEditor::getMouseOverContent() const
+{
+       return m_splitScroll.getScrollX() + (Fl::event_x() - G_GUI_OUTER_MARGIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBaseActionEditor::cb_zoomIn(Fl_Widget* /*w*/, void* p) { ((gdBaseActionEditor*)p)->zoomIn(); }
+void gdBaseActionEditor::cb_zoomOut(Fl_Widget* /*w*/, void* p) { ((gdBaseActionEditor*)p)->zoomOut(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdBaseActionEditor::computeWidth(Frame framesInSeq, Frame framesInLoop)
+{
+       fullWidth = frameToPixel(framesInSeq);
+       loopWidth = frameToPixel(framesInLoop);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Pixel gdBaseActionEditor::frameToPixel(Frame f) const
+{
+       return f / m_ratio;
+}
+
+Frame gdBaseActionEditor::pixelToFrame(Pixel p, Frame framesInBeat, bool snap) const
+{
+       return snap ? gridTool.getSnapFrame(p * m_ratio, framesInBeat) : p * m_ratio;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBaseActionEditor::zoomIn()
+{
+       // Explicit type std::max<int> to fix MINMAX macro hell on Windows
+       zoomAbout([&r = m_ratio]() { return std::max<int>(r / RATIO_STEP, MIN_RATIO); });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBaseActionEditor::zoomOut()
+{
+       // Explicit type std::max<int> to fix MINMAX macro hell on Windows
+       zoomAbout([&r = m_ratio]() { return std::min<int>(r * RATIO_STEP, MAX_RATIO); });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBaseActionEditor::prepareWindow()
+{
+       u::gui::setFavicon(this);
+
+       std::string l = "Action Editor";
+       if (m_data.channelName != "")
+               l += " - " + m_data.channelName;
+       copy_label(l.c_str());
+
+       set_non_modal();
+       size_range(640, 284);
+
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int gdBaseActionEditor::handle(int e)
+{
+       switch (e)
+       {
+       case FL_MOUSEWHEEL:
+               Fl::event_dy() == -1 ? zoomIn() : zoomOut();
+               return 1;
+       default:
+               return Fl_Group::handle(e);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBaseActionEditor::draw()
+{
+       gdWindow::draw();
+
+       const geompp::Rect splitBounds = m_splitScroll.getBoundsNoScrollbar();
+       const geompp::Line playhead    = splitBounds.getHeightAsLine().withX(currentFrameToPixel());
+
+       if (splitBounds.contains(playhead))
+               drawLine(playhead, G_COLOR_LIGHT_2);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBaseActionEditor::zoomAbout(std::function<float()> f)
+{
+       const float ratioPrev = m_ratio;
+       const int   minWidth  = w() - (G_GUI_OUTER_MARGIN * 2);
+
+       m_ratio = f();
+
+       /* Make sure the new content width doesn't underflow the window space (i.e. 
+       the minimum width allowed). */
+
+       if (frameToPixel(m_data.framesInSeq) < minWidth)
+       {
+               m_ratio = m_data.framesInSeq / static_cast<float>(minWidth);
+               m_splitScroll.setScrollX(0);
+       }
+
+       /* 1. Store the current x-position, then the new x-position affected by the
+       zoom change. */
+
+       const int mpre = getMouseOverContent();
+       const int mnow = mpre / (m_ratio / ratioPrev);
+
+       /* 2. Rebuild everything and adjust scrolling given the change occurred in
+       the x-position. This effectively centers the view on the mouse cursor. */
+
+       rebuild();
+       m_splitScroll.setScrollX(m_splitScroll.getScrollX() + (mnow - mpre));
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBaseActionEditor::refresh()
+{
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Pixel gdBaseActionEditor::currentFrameToPixel() const
+{
+       return (frameToPixel(m_data.getCurrentFrame()) + m_splitScroll.x()) - m_splitScroll.getScrollX();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/actionEditor/baseActionEditor.h b/src/gui/dialogs/actionEditor/baseActionEditor.h
new file mode 100644 (file)
index 0000000..3ff2f38
--- /dev/null
@@ -0,0 +1,113 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_BASE_ACTION_EDITOR_H
+#define GD_BASE_ACTION_EDITOR_H
+
+#include "core/conf.h"
+#include "glue/actionEditor.h"
+#include "gui/dialogs/window.h"
+#include "gui/elems/actionEditor/gridTool.h"
+#include "gui/elems/actionEditor/splitScroll.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/pack.h"
+
+namespace giada::m
+{
+class Channel;
+struct Action;
+} // namespace giada::m
+
+namespace giada::v
+{
+class gdBaseActionEditor : public gdWindow
+{
+public:
+       virtual ~gdBaseActionEditor();
+
+       int  handle(int e) override;
+       void draw() override;
+
+       Pixel frameToPixel(Frame f) const;
+       Frame pixelToFrame(Pixel p, Frame framesInBeat, bool snap = true) const;
+
+       ID channelId;
+
+       geGridTool gridTool;
+       geButton   zoomInBtn;
+       geButton   zoomOutBtn;
+
+       Pixel fullWidth; // Full widgets width, i.e. scaled-down full sequencer
+       Pixel loopWidth; // Loop width, i.e. scaled-down sequencer range
+
+protected:
+       static constexpr float MIN_RATIO  = 25.0f;
+       static constexpr float MAX_RATIO  = 40000.0f;
+       static constexpr float RATIO_STEP = 1.5f;
+
+       gdBaseActionEditor(ID channelId, m::Conf::Data&);
+
+       /* getMouseOverContent
+       Returns mouse x-position relative to the viewport content. */
+
+       int getMouseOverContent() const;
+
+       static void cb_zoomIn(Fl_Widget* w, void* p);
+       static void cb_zoomOut(Fl_Widget* w, void* p);
+       void        zoomIn();
+       void        zoomOut();
+
+       /* computeWidth
+       Computes total width, in pixel. */
+
+       void computeWidth(Frame framesInSeq, Frame framesInLoop);
+
+       /* prepareWindow
+       Initializes window (favicon, limits, ...). */
+
+       void prepareWindow();
+
+       gePack        m_barTop;
+       geSplitScroll m_splitScroll;
+
+       c::actionEditor::Data m_data;
+       m::Conf::Data&        m_conf;
+
+private:
+       void refresh() override;
+
+       /* zoomAbout
+       Zooms and centers the viewport around the mouse cursor. Wants a function to 
+       apply to the current ratio. */
+
+       void zoomAbout(std::function<float()> f);
+
+       Pixel currentFrameToPixel() const;
+
+       float m_ratio;
+};
+} // namespace giada::v
+#endif
diff --git a/src/gui/dialogs/actionEditor/midiActionEditor.cpp b/src/gui/dialogs/actionEditor/midiActionEditor.cpp
new file mode 100644 (file)
index 0000000..a270a10
--- /dev/null
@@ -0,0 +1,81 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiActionEditor.h"
+#include "core/conf.h"
+#include "glue/actionEditor.h"
+#include "glue/channel.h"
+#include "gui/elems/basics/box.h"
+
+namespace giada::v
+{
+gdMidiActionEditor::gdMidiActionEditor(ID channelId, m::Conf::Data& conf)
+: gdBaseActionEditor(channelId, conf)
+, m_barPadding(0, 0, w() - 150, G_GUI_UNIT)
+, m_pianoRoll(0, 0, this)
+, m_velocityEditor(0, 0, this)
+{
+       end();
+
+       m_barTop.add(&gridTool);
+       m_barTop.add(&m_barPadding);
+       m_barTop.add(&zoomInBtn);
+       m_barTop.add(&zoomOutBtn);
+       m_barTop.resizable(m_barPadding);
+
+       m_splitScroll.addWidgets(m_pianoRoll, m_velocityEditor, conf.actionEditorSplitH);
+
+       if (conf.actionEditorPianoRollY != -1)
+               m_splitScroll.setScrollY(conf.actionEditorPianoRollY);
+
+       resizable(m_splitScroll); // Make it resizable only once filled with widgets
+
+       prepareWindow();
+       rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdMidiActionEditor::~gdMidiActionEditor()
+{
+       m_conf.actionEditorPianoRollY = m_splitScroll.getScrollY();
+       m_barTop.remove(gridTool);
+       m_barTop.remove(zoomInBtn);
+       m_barTop.remove(zoomOutBtn);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiActionEditor::rebuild()
+{
+       m_data = c::actionEditor::getData(channelId);
+
+       computeWidth(m_data.framesInSeq, m_data.framesInLoop);
+
+       m_pianoRoll.rebuild(m_data);
+       m_velocityEditor.rebuild(m_data);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/actionEditor/midiActionEditor.h b/src/gui/dialogs/actionEditor/midiActionEditor.h
new file mode 100644 (file)
index 0000000..4722d65
--- /dev/null
@@ -0,0 +1,52 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MIDI_ACTION_EDITOR_H
+#define GD_MIDI_ACTION_EDITOR_H
+
+#include "baseActionEditor.h"
+#include "gui/elems/actionEditor/pianoRoll.h"
+#include "gui/elems/actionEditor/velocityEditor.h"
+#include "gui/elems/basics/box.h"
+
+namespace giada::v
+{
+class gdMidiActionEditor : public gdBaseActionEditor
+{
+public:
+       gdMidiActionEditor(ID channelId, m::Conf::Data&);
+       ~gdMidiActionEditor();
+
+       void rebuild() override;
+
+private:
+       geBox            m_barPadding;
+       gePianoRoll      m_pianoRoll;
+       geVelocityEditor m_velocityEditor;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/actionEditor/sampleActionEditor.cpp b/src/gui/dialogs/actionEditor/sampleActionEditor.cpp
new file mode 100644 (file)
index 0000000..944d4c4
--- /dev/null
@@ -0,0 +1,116 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "sampleActionEditor.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/midiEvent.h"
+#include "core/model/model.h"
+#include "glue/actionEditor.h"
+#include "glue/channel.h"
+#include "gui/elems/basics/box.h"
+#include <string>
+
+namespace giada::v
+{
+gdSampleActionEditor::gdSampleActionEditor(ID channelId, m::Conf::Data& conf)
+: gdBaseActionEditor(channelId, conf)
+, m_barPadding(0, 0, w() - 232, G_GUI_UNIT)
+, m_sampleActionEditor(0, 0, this)
+, m_envelopeEditor(0, 0, "Volume", this)
+, m_actionType(0, 0, 80, G_GUI_UNIT)
+{
+       end();
+
+       m_barTop.add(&m_actionType);
+       m_barTop.add(&gridTool);
+       m_barTop.add(&m_barPadding);
+       m_barTop.add(&zoomInBtn);
+       m_barTop.add(&zoomOutBtn);
+       m_barTop.resizable(m_barPadding);
+
+       m_actionType.addItem("Key press");
+       m_actionType.addItem("Key release");
+       m_actionType.addItem("Stop sample");
+       m_actionType.showItem(0);
+       m_actionType.copy_tooltip("Action type to add");
+       if (!canChangeActionType())
+               m_actionType.deactivate();
+
+       m_splitScroll.addWidgets(m_sampleActionEditor, m_envelopeEditor, conf.actionEditorSplitH);
+
+       resizable(m_splitScroll); // Make it resizable only once filled with widgets
+
+       prepareWindow();
+       rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdSampleActionEditor::~gdSampleActionEditor()
+{
+       m_barTop.remove(m_actionType);
+       m_barTop.remove(gridTool);
+       m_barTop.remove(zoomInBtn);
+       m_barTop.remove(zoomOutBtn);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool gdSampleActionEditor::canChangeActionType()
+{
+       return m_data.sample->channelMode != SamplePlayerMode::SINGLE_PRESS &&
+              m_data.sample->isLoopMode == false;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleActionEditor::rebuild()
+{
+       m_data = c::actionEditor::getData(channelId);
+
+       canChangeActionType() ? m_actionType.activate() : m_actionType.deactivate();
+       computeWidth(m_data.framesInSeq, m_data.framesInLoop);
+
+       m_sampleActionEditor.rebuild(m_data);
+       m_envelopeEditor.rebuild(m_data);
+}
+
+/* -------------------------------------------------------------------------- */
+
+int gdSampleActionEditor::getActionType() const
+{
+       if (m_actionType.getSelectedId() == 0)
+               return m::MidiEvent::NOTE_ON;
+       else if (m_actionType.getSelectedId() == 1)
+               return m::MidiEvent::NOTE_OFF;
+       else if (m_actionType.getSelectedId() == 2)
+               return m::MidiEvent::NOTE_KILL;
+
+       assert(false);
+       return -1;
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/actionEditor/sampleActionEditor.h b/src/gui/dialogs/actionEditor/sampleActionEditor.h
new file mode 100644 (file)
index 0000000..a1f42df
--- /dev/null
@@ -0,0 +1,58 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_SAMPLE_ACTION_EDITOR_H
+#define GD_SAMPLE_ACTION_EDITOR_H
+
+#include "baseActionEditor.h"
+#include "gui/elems/actionEditor/envelopeEditor.h"
+#include "gui/elems/actionEditor/sampleActionEditor.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/choice.h"
+
+namespace giada::v
+{
+class gdSampleActionEditor : public gdBaseActionEditor
+{
+public:
+       gdSampleActionEditor(ID channelId, m::Conf::Data&);
+       ~gdSampleActionEditor();
+
+       void rebuild() override;
+
+       int getActionType() const;
+
+private:
+       bool canChangeActionType();
+
+       geBox                m_barPadding;
+       geSampleActionEditor m_sampleActionEditor;
+       geEnvelopeEditor     m_envelopeEditor;
+       geChoice             m_actionType;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/beatsInput.cpp b/src/gui/dialogs/beatsInput.cpp
new file mode 100644 (file)
index 0000000..c777498
--- /dev/null
@@ -0,0 +1,82 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "beatsInput.h"
+#include "core/const.h"
+#include "glue/main.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/input.h"
+#include "mainWindow.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <cstring>
+
+extern giada::v::gdMainWindow* mainWin;
+
+namespace giada::v
+{
+gdBeatsInput::gdBeatsInput(int beats, int bars)
+: gdWindow(u::gui::centerWindowX(180), u::gui::centerWindowY(36), 180, 36, "Beats")
+{
+       set_modal();
+
+       begin();
+       m_beats = new geInput(8, 8, 43, G_GUI_UNIT);
+       m_bars  = new geInput(m_beats->x() + m_beats->w() + 4, 8, 43, G_GUI_UNIT);
+       m_ok    = new geButton(m_bars->x() + m_bars->w() + 4, 8, 70, G_GUI_UNIT, "Ok");
+       end();
+
+       m_beats->maximum_size(2);
+       m_beats->value(std::to_string(beats).c_str());
+       m_beats->type(FL_INT_INPUT);
+
+       m_bars->maximum_size(2);
+       m_bars->value(std::to_string(bars).c_str());
+       m_bars->type(FL_INT_INPUT);
+
+       m_ok->shortcut(FL_Enter);
+       m_ok->callback(cb_update, (void*)this);
+
+       u::gui::setFavicon(this);
+       setId(WID_BEATS);
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBeatsInput::cb_update(Fl_Widget* /*w*/, void* p) { ((gdBeatsInput*)p)->cb_update(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdBeatsInput::cb_update()
+{
+       if (!strcmp(m_beats->value(), "") || !strcmp(m_bars->value(), ""))
+               return;
+       c::main::setBeats(atoi(m_beats->value()), atoi(m_bars->value()));
+       do_callback();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/beatsInput.h b/src/gui/dialogs/beatsInput.h
new file mode 100644 (file)
index 0000000..5a68724
--- /dev/null
@@ -0,0 +1,53 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_BEATSINPUT_H
+#define GD_BEATSINPUT_H
+
+#include "window.h"
+
+class geInput;
+class geCheck;
+
+namespace giada::v
+{
+class geButton;
+class gdBeatsInput : public gdWindow
+{
+public:
+       gdBeatsInput(int beats, int bars);
+
+private:
+       static void cb_update(Fl_Widget* /*w*/, void* p);
+       void        cb_update();
+
+       geInput*  m_beats;
+       geInput*  m_bars;
+       geButton* m_ok;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/bpmInput.cpp b/src/gui/dialogs/bpmInput.cpp
new file mode 100644 (file)
index 0000000..b84c655
--- /dev/null
@@ -0,0 +1,85 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "bpmInput.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/mixer.h"
+#include "glue/main.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/input.h"
+#include "mainWindow.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <cstring>
+
+extern giada::v::gdMainWindow* mainWin;
+
+namespace giada::v
+{
+gdBpmInput::gdBpmInput(const char* label)
+: gdWindow(u::gui::centerWindowX(144), u::gui::centerWindowY(36), 144, 36, "Bpm")
+{
+       set_modal();
+
+       begin();
+       input_a = new geInput(8, 8, 30, G_GUI_UNIT);
+       input_b = new geInput(42, 8, 20, G_GUI_UNIT);
+       ok      = new geButton(66, 8, 70, G_GUI_UNIT, "Ok");
+       end();
+
+       std::vector<std::string> parts = u::string::split(label, ".");
+
+       input_a->maximum_size(3);
+       input_a->type(FL_INT_INPUT);
+       input_a->value(parts[0].c_str());
+
+       input_b->maximum_size(1);
+       input_b->type(FL_INT_INPUT);
+       input_b->value(parts[1].c_str());
+
+       ok->shortcut(FL_Enter);
+       ok->callback(cb_update, (void*)this);
+
+       u::gui::setFavicon(this);
+       setId(WID_BPM);
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBpmInput::cb_update(Fl_Widget* /*w*/, void* p) { ((gdBpmInput*)p)->cb_update(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdBpmInput::cb_update()
+{
+       if (strcmp(input_a->value(), "") == 0)
+               return;
+       c::main::setBpm(input_a->value(), input_b->value());
+       do_callback();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/bpmInput.h b/src/gui/dialogs/bpmInput.h
new file mode 100644 (file)
index 0000000..8658e9a
--- /dev/null
@@ -0,0 +1,52 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_BPMINPUT_H
+#define GD_BPMINPUT_H
+
+#include "window.h"
+
+class geInput;
+
+namespace giada::v
+{
+class geButton;
+class gdBpmInput : public gdWindow
+{
+public:
+       gdBpmInput(const char* label); // pointer to mainWin->timing->bpm->label()
+
+private:
+       static void cb_update(Fl_Widget* /*w*/, void* p);
+       void        cb_update();
+
+       geInput*  input_a;
+       geInput*  input_b;
+       geButton* ok;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/browser/browserBase.cpp b/src/gui/dialogs/browser/browserBase.cpp
new file mode 100644 (file)
index 0000000..1ed0028
--- /dev/null
@@ -0,0 +1,156 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/browser/browserBase.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/basics/progress.h"
+#include "gui/elems/fileBrowser.h"
+#include "utils/fs.h"
+#include "utils/gui.h"
+
+namespace giada::v
+{
+gdBrowserBase::gdBrowserBase(const std::string& title, const std::string& path,
+    std::function<void(void*)> callback, ID channelId, m::Conf::Data& c)
+: gdWindow(c.browserX, c.browserY, c.browserW,
+      c.browserH, title.c_str())
+, m_callback(callback)
+, m_conf(c)
+, m_channelId(channelId)
+{
+       set_non_modal();
+
+       begin();
+       
+       groupTop    = new Fl_Group(8, 8, w() - 16, 48);
+       hiddenFiles = new geCheck(groupTop->x(), groupTop->y(), 400, 20, "Show hidden files");
+       where       = new geInput(groupTop->x(), hiddenFiles->y() + hiddenFiles->h() + 8, 20, 20);
+       updir       = new geButton(groupTop->x() + groupTop->w() - 20, where->y(), 20, 20, "", updirOff_xpm, updirOn_xpm);
+       groupTop->end();
+       groupTop->resizable(where);
+
+       hiddenFiles->callback(cb_toggleHiddenFiles, (void*)this);
+
+       where->readonly(true);
+       where->cursor_color(G_COLOR_BLACK);
+       where->value(path.c_str());
+
+       updir->callback(cb_up, (void*)this);
+
+       browser = new geFileBrowser(8, groupTop->y() + groupTop->h() + 8, w() - 16, h() - 101);
+       browser->loadDir(path);
+       if (path == m_conf.browserLastPath)
+               browser->preselect(m_conf.browserPosition, m_conf.browserLastValue);
+
+       Fl_Group* groupButtons = new Fl_Group(8, browser->y() + browser->h() + 8, w() - 16, 20);
+       ok                     = new geButton(w() - 88, groupButtons->y(), 80, 20);
+       cancel                 = new geButton(w() - ok->w() - 96, groupButtons->y(), 80, 20, "Cancel");
+       geBox* spacer          = new geBox(8, groupButtons->y(), cancel->x() - 16, 20);
+       groupButtons->resizable(spacer);
+       groupButtons->end();
+
+       end();
+
+       cancel->callback(cb_close, (void*)this);
+
+       resizable(browser);
+       size_range(320, 200);
+
+       u::gui::setFavicon(this);
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdBrowserBase::~gdBrowserBase()
+{
+       m_conf.browserX         = x();
+       m_conf.browserY         = y();
+       m_conf.browserW         = w();
+       m_conf.browserH         = h();
+       m_conf.browserPosition  = browser->position();
+       m_conf.browserLastPath  = browser->getCurrentDir();
+       m_conf.browserLastValue = browser->value();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserBase::cb_up(Fl_Widget* /*v*/, void* p) { ((gdBrowserBase*)p)->cb_up(); }
+void gdBrowserBase::cb_close(Fl_Widget* /*v*/, void* p) { ((gdBrowserBase*)p)->cb_close(); }
+void gdBrowserBase::cb_toggleHiddenFiles(Fl_Widget* /*v*/, void* p) { ((gdBrowserBase*)p)->cb_toggleHiddenFiles(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserBase::cb_up()
+{
+       browser->loadDir(u::fs::getUpDir(browser->getCurrentDir()));
+       where->value(browser->getCurrentDir().c_str());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserBase::cb_close()
+{
+       do_callback();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserBase::cb_toggleHiddenFiles()
+{
+       browser->toggleHiddenFiles();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string gdBrowserBase::getCurrentPath() const
+{
+       return where->value();
+}
+
+ID gdBrowserBase::getChannelId() const
+{
+       return m_channelId;
+}
+
+std::string gdBrowserBase::getSelectedItem() const
+{
+       return browser->getSelectedItem();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserBase::fireCallback() const
+{
+       m_callback((void*)this);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/browser/browserBase.h b/src/gui/dialogs/browser/browserBase.h
new file mode 100644 (file)
index 0000000..af92f9c
--- /dev/null
@@ -0,0 +1,92 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_BROWSER_BASE_H
+#define GD_BROWSER_BASE_H
+
+#include "core/conf.h"
+#include "core/types.h"
+#include "gui/dialogs/window.h"
+#include <functional>
+#include <string>
+
+class Fl_Group;
+class geCheck;
+class geInput;
+
+namespace giada::m
+{
+class Channel;
+}
+
+namespace giada::v
+{
+class geButton;
+class geFileBrowser;
+class gdBrowserBase : public gdWindow
+{
+public:
+       ~gdBrowserBase();
+
+       /* getSelectedItem
+       Returns the full path of the selected file. */
+
+       std::string getSelectedItem() const;
+
+       std::string getCurrentPath() const;
+       ID          getChannelId() const;
+       void        fireCallback() const;
+
+protected:
+       gdBrowserBase(const std::string& title, const std::string& path,
+           std::function<void(void*)> f, ID channelId, m::Conf::Data&);
+
+       static void cb_up(Fl_Widget* /*w*/, void* p);
+       static void cb_close(Fl_Widget* /*w*/, void* p);
+       static void cb_toggleHiddenFiles(Fl_Widget* /*w*/, void* p);
+       void        cb_up();
+       void        cb_close();
+       void        cb_toggleHiddenFiles();
+
+       /* m_callback
+       Fired when the save/load button is pressed. */
+
+       std::function<void(void*)> m_callback;
+
+       m::Conf::Data& m_conf;
+       ID             m_channelId;
+
+       Fl_Group*      groupTop;
+       geCheck*       hiddenFiles;
+       geFileBrowser* browser;
+       geButton*      ok;
+       geButton*      cancel;
+       geInput*       where;
+       geButton*      updir;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/browser/browserDir.cpp b/src/gui/dialogs/browser/browserDir.cpp
new file mode 100644 (file)
index 0000000..69ea65a
--- /dev/null
@@ -0,0 +1,77 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "browserDir.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/fileBrowser.h"
+#include "utils/fs.h"
+
+namespace giada::v
+{
+gdBrowserDir::gdBrowserDir(const std::string& title, const std::string& path,
+    std::function<void(void*)> cb, m::Conf::Data& conf)
+: gdBrowserBase(title, path, cb, 0, conf)
+{
+       where->size(groupTop->w() - updir->w() - 8, 20);
+
+       browser->callback(cb_down, (void*)this);
+
+       ok->label("Select");
+       ok->callback(cb_load, (void*)this);
+       ok->shortcut(FL_ENTER);
+
+       /* On OS X the 'where' input doesn't get resized properly on startup. Let's 
+       force it. */
+
+       where->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserDir::cb_load(Fl_Widget* /*v*/, void* p) { ((gdBrowserDir*)p)->cb_load(); }
+void gdBrowserDir::cb_down(Fl_Widget* /*v*/, void* p) { ((gdBrowserDir*)p)->cb_down(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserDir::cb_load()
+{
+       fireCallback();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserDir::cb_down()
+{
+       std::string path = browser->getSelectedItem();
+
+       if (path.empty() || !u::fs::isDir(path)) // when click on an empty area or not a dir
+               return;
+
+       browser->loadDir(path);
+       where->value(browser->getCurrentDir().c_str());
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/browser/browserDir.h b/src/gui/dialogs/browser/browserDir.h
new file mode 100644 (file)
index 0000000..218d002
--- /dev/null
@@ -0,0 +1,49 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_BROWSER_DIR_H
+#define GD_BROWSER_DIR_H
+
+#include "core/conf.h"
+#include "browserBase.h"
+
+namespace giada::v
+{
+class gdBrowserDir : public gdBrowserBase
+{
+public:
+       gdBrowserDir(const std::string& title, const std::string& path,
+           std::function<void(void*)> cb, m::Conf::Data&);
+
+private:
+       static void cb_load(Fl_Widget* /*w*/, void* p);
+       static void cb_down(Fl_Widget* /*w*/, void* p);
+       void        cb_load();
+       void        cb_down();
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/browser/browserLoad.cpp b/src/gui/dialogs/browser/browserLoad.cpp
new file mode 100644 (file)
index 0000000..5b4d90f
--- /dev/null
@@ -0,0 +1,77 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "browserLoad.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/fileBrowser.h"
+#include "utils/fs.h"
+
+namespace giada::v
+{
+gdBrowserLoad::gdBrowserLoad(const std::string& title, const std::string& path,
+    std::function<void(void*)> cb, ID channelId, m::Conf::Data& conf)
+: gdBrowserBase(title, path, cb, channelId, conf)
+{
+       where->size(groupTop->w() - updir->w() - 8, 20);
+
+       browser->callback(cb_down, (void*)this);
+
+       ok->label("Load");
+       ok->callback(cb_load, (void*)this);
+       ok->shortcut(FL_ENTER);
+
+       /* On OS X the 'where' input doesn't get resized properly on startup. Let's 
+       force it. */
+
+       where->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserLoad::cb_load(Fl_Widget* /*v*/, void* p) { ((gdBrowserLoad*)p)->cb_load(); }
+void gdBrowserLoad::cb_down(Fl_Widget* /*v*/, void* p) { ((gdBrowserLoad*)p)->cb_down(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserLoad::cb_load()
+{
+       fireCallback();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserLoad::cb_down()
+{
+       std::string path = browser->getSelectedItem();
+
+       if (path.empty() || !u::fs::isDir(path)) // when click on an empty area or not a dir
+               return;
+
+       browser->loadDir(path);
+       where->value(browser->getCurrentDir().c_str());
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/browser/browserLoad.h b/src/gui/dialogs/browser/browserLoad.h
new file mode 100644 (file)
index 0000000..828276d
--- /dev/null
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_BROWSER_LOAD_H
+#define GD_BROWSER_LOAD_H
+
+#include "browserBase.h"
+#include "core/conf.h"
+
+namespace giada::m
+{
+class Channel;
+}
+
+namespace giada::v
+{
+class gdBrowserLoad : public gdBrowserBase
+{
+public:
+       gdBrowserLoad(const std::string& title, const std::string& path,
+           std::function<void(void*)> cb, ID channelId, m::Conf::Data&);
+
+private:
+       static void cb_load(Fl_Widget* /*w*/, void* p);
+       static void cb_down(Fl_Widget* /*w*/, void* p);
+       void        cb_load();
+       void        cb_down();
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/browser/browserSave.cpp b/src/gui/dialogs/browser/browserSave.cpp
new file mode 100644 (file)
index 0000000..56f18c0
--- /dev/null
@@ -0,0 +1,98 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "browserSave.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/fileBrowser.h"
+#include "utils/fs.h"
+
+namespace giada::v
+{
+gdBrowserSave::gdBrowserSave(const std::string& title, const std::string& path,
+    const std::string& name_, std::function<void(void*)> cb, ID channelId,
+    m::Conf::Data& conf)
+: gdBrowserBase(title, path, cb, channelId, conf)
+{
+       where->size(groupTop->w() - 236, 20);
+
+       name = new geInput(where->x() + where->w() + 8, where->y(), 200, 20);
+       name->value(name_.c_str());
+       groupTop->add(name);
+
+       browser->callback(cb_down, (void*)this);
+
+       ok->label("Save");
+       ok->callback(cb_save, (void*)this);
+       ok->shortcut(FL_ENTER);
+
+       /* On OS X the 'where' and 'name' inputs don't get resized properly on startup. 
+       Let's force them. */
+
+       where->redraw();
+       name->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserSave::cb_save(Fl_Widget* /*v*/, void* p) { ((gdBrowserSave*)p)->cb_save(); }
+void gdBrowserSave::cb_down(Fl_Widget* /*v*/, void* p) { ((gdBrowserSave*)p)->cb_down(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserSave::cb_down()
+{
+       std::string path = browser->getSelectedItem();
+
+       if (path.empty()) // when click on an empty area
+               return;
+
+       /* if the selected item is a directory just load its content. If it's a file
+        * use it as the file name (i.e. fill name->value()). */
+
+       if (u::fs::isDir(path))
+       {
+               browser->loadDir(path);
+               where->value(browser->getCurrentDir().c_str());
+       }
+       else
+               name->value(browser->getSelectedItem(false).c_str());
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string gdBrowserSave::getName() const
+{
+       return name->value();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdBrowserSave::cb_save()
+{
+       fireCallback();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/browser/browserSave.h b/src/gui/dialogs/browser/browserSave.h
new file mode 100644 (file)
index 0000000..8f07133
--- /dev/null
@@ -0,0 +1,61 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_BROWSER_SAVE_H
+#define GD_BROWSER_SAVE_H
+
+#include "core/conf.h"
+#include "browserBase.h"
+
+class geInput;
+
+namespace giada::m
+{
+class Channel;
+}
+
+namespace giada::v
+{
+class gdBrowserSave : public gdBrowserBase
+{
+public:
+       gdBrowserSave(const std::string& title, const std::string& path,
+           const std::string& name, std::function<void(void*)> cb,
+           ID channelId, m::Conf::Data&);
+
+       std::string getName() const;
+
+private:
+       geInput* name;
+
+       static void cb_down(Fl_Widget* /*w*/, void* p);
+       static void cb_save(Fl_Widget* /*w*/, void* p);
+       void        cb_down();
+       void        cb_save();
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/channelNameInput.cpp b/src/gui/dialogs/channelNameInput.cpp
new file mode 100644 (file)
index 0000000..ed9ac9c
--- /dev/null
@@ -0,0 +1,85 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "channelNameInput.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "glue/channel.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/input.h"
+#include "utils/gui.h"
+
+namespace giada
+{
+namespace v
+{
+gdChannelNameInput::gdChannelNameInput(const c::channel::Data& d)
+: gdWindow(u::gui::centerWindowX(400), u::gui::centerWindowY(64), 400, 64, "New channel name")
+, m_data(d)
+{
+       set_modal();
+
+       begin();
+       m_name   = new geInput(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN, w() - (G_GUI_OUTER_MARGIN * 2), G_GUI_UNIT);
+       m_ok     = new geButton(w() - 70 - G_GUI_OUTER_MARGIN, m_name->y() + m_name->h() + G_GUI_OUTER_MARGIN, 70, G_GUI_UNIT, "Ok");
+       m_cancel = new geButton(m_ok->x() - 70 - G_GUI_OUTER_MARGIN, m_ok->y(), 70, G_GUI_UNIT, "Cancel");
+       end();
+
+       m_name->value(m_data.name.c_str());
+
+       m_ok->shortcut(FL_Enter);
+       m_ok->callback(cb_update, (void*)this);
+
+       m_cancel->callback(cb_cancel, (void*)this);
+
+       u::gui::setFavicon(this);
+       setId(WID_SAMPLE_NAME);
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdChannelNameInput::cb_update(Fl_Widget* /*w*/, void* p) { ((gdChannelNameInput*)p)->cb_update(); }
+void gdChannelNameInput::cb_cancel(Fl_Widget* /*w*/, void* p) { ((gdChannelNameInput*)p)->cb_cancel(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdChannelNameInput::cb_cancel()
+{
+       do_callback();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdChannelNameInput::cb_update()
+{
+       c::channel::setName(m_data.id, m_name->value());
+       do_callback();
+}
+
+} // namespace v
+} // namespace giada
diff --git a/src/gui/dialogs/channelNameInput.h b/src/gui/dialogs/channelNameInput.h
new file mode 100644 (file)
index 0000000..c9286ae
--- /dev/null
@@ -0,0 +1,61 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_CHANNEL_NAME_INPUT_H
+#define GD_CHANNEL_NAME_INPUT_H
+
+#include "window.h"
+
+class geInput;
+
+namespace giada::c::channel
+{
+struct Data;
+}
+
+namespace giada::v
+{
+class geButton;
+class gdChannelNameInput : public gdWindow
+{
+public:
+       gdChannelNameInput(const c::channel::Data& d);
+
+private:
+       static void cb_update(Fl_Widget* /*w*/, void* p);
+       static void cb_cancel(Fl_Widget* /*w*/, void* p);
+       void        cb_update();
+       void        cb_cancel();
+
+       const c::channel::Data& m_data;
+
+       geInput*  m_name;
+       geButton* m_ok;
+       geButton* m_cancel;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/config.cpp b/src/gui/dialogs/config.cpp
new file mode 100644 (file)
index 0000000..29048a8
--- /dev/null
@@ -0,0 +1,112 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/config.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/flex.h"
+#include "gui/elems/basics/tabs.h"
+#include "gui/elems/config/tabAudio.h"
+#include "gui/elems/config/tabBehaviors.h"
+#include "gui/elems/config/tabBindings.h"
+#include "gui/elems/config/tabMidi.h"
+#include "gui/elems/config/tabMisc.h"
+#include "gui/elems/config/tabPlugins.h"
+#include "utils/gui.h"
+
+namespace giada::v
+{
+gdConfig::gdConfig(int w, int h, m::Conf::Data& conf)
+: gdWindow(u::gui::getCenterWinBounds(w, h), "Configuration")
+{
+       const geompp::Rect<int> bounds = getContentBounds().reduced(G_GUI_OUTER_MARGIN);
+
+       geFlex* container = new geFlex(bounds, Direction::VERTICAL, G_GUI_OUTER_MARGIN);
+       {
+               geTabs* tabs = new geTabs(bounds);
+               {
+                       tabAudio     = new geTabAudio(bounds);
+                       tabMidi      = new geTabMidi(bounds);
+                       tabBehaviors = new geTabBehaviors(bounds, conf);
+                       tabMisc      = new geTabMisc(bounds);
+                       tabBindings  = new geTabBindings(bounds, conf);
+#ifdef WITH_VST
+                       tabPlugins = new geTabPlugins(bounds);
+#endif
+                       tabs->add(tabAudio);
+                       tabs->add(tabMidi);
+                       tabs->add(tabBehaviors);
+#ifdef WITH_VST
+                       tabs->add(tabPlugins);
+#endif
+                       tabs->add(tabBindings);
+                       tabs->add(tabMisc);
+               }
+
+               geFlex* footer = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       geButton* save   = new geButton("Save");
+                       geButton* cancel = new geButton("Cancel");
+                       save->onClick    = [this]() { saveConfig(); };
+                       cancel->onClick  = [this]() { do_callback(); };
+
+                       footer->add(new geBox()); // Spacer
+                       footer->add(cancel, 80);
+                       footer->add(save, 80);
+                       footer->end();
+               }
+
+               container->add(tabs);
+               container->add(footer, G_GUI_UNIT);
+               container->end();
+       }
+
+       add(container);
+       resizable(container);
+       size_range(w, h);
+
+       u::gui::setFavicon(this);
+       setId(WID_CONFIG);
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdConfig::saveConfig()
+{
+       tabAudio->save();
+       tabBehaviors->save();
+       tabMidi->save();
+       tabMisc->save();
+#ifdef WITH_VST
+       tabPlugins->save();
+#endif
+       do_callback();
+}
+} // namespace giada::v
diff --git a/src/gui/dialogs/config.h b/src/gui/dialogs/config.h
new file mode 100644 (file)
index 0000000..9c4e5c2
--- /dev/null
@@ -0,0 +1,63 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_CONFIG_H
+#define GD_CONFIG_H
+
+#include "core/conf.h"
+#include "window.h"
+
+namespace giada::v
+{
+class geButton;
+class geTabAudio;
+class geTabBehaviors;
+class geTabMidi;
+class geTabMisc;
+class geTabBindings;
+#ifdef WITH_VST
+class geTabPlugins;
+#endif
+class gdConfig : public gdWindow
+{
+public:
+       gdConfig(int w, int h, m::Conf::Data&);
+
+       geTabAudio*     tabAudio;
+       geTabBehaviors* tabBehaviors;
+       geTabMidi*      tabMidi;
+       geTabMisc*      tabMisc;
+       geTabBindings*  tabBindings;
+#ifdef WITH_VST
+       geTabPlugins* tabPlugins;
+#endif
+
+private:
+       void saveConfig();
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/keyGrabber.cpp b/src/gui/dialogs/keyGrabber.cpp
new file mode 100644 (file)
index 0000000..51fae3c
--- /dev/null
@@ -0,0 +1,119 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/keyGrabber.h"
+#include "core/conf.h"
+#include "glue/channel.h"
+#include "glue/io.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/flex.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include <cassert>
+
+namespace giada::v
+{
+gdKeyGrabber::gdKeyGrabber(int key)
+: gdWindow(300, 126, "Key configuration")
+, onSetKey(nullptr)
+, m_key(key)
+{
+       geFlex* container = new geFlex(getContentBounds().reduced({G_GUI_OUTER_MARGIN}), Direction::VERTICAL, G_GUI_OUTER_MARGIN);
+       {
+               m_text = new geBox();
+
+               geFlex* footer = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       m_clear  = new geButton("Clear");
+                       m_cancel = new geButton("Close");
+
+                       footer->add(new geBox()); // Spacer
+                       footer->add(m_clear, 80);
+                       footer->add(m_cancel, 80);
+                       footer->end();
+               }
+
+               container->add(m_text);
+               container->add(footer, G_GUI_UNIT);
+               container->end();
+       }
+
+       add(container);
+
+       m_clear->onClick = [this]() {
+               assert(onSetKey != nullptr);
+
+               m_key = 0;
+               onSetKey(m_key);
+               rebuild();
+       };
+
+       m_cancel->onClick = [this]() {
+               do_callback();
+       };
+
+       rebuild();
+
+       u::gui::setFavicon(this);
+       set_modal();
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdKeyGrabber::rebuild()
+{
+       std::string tmp = "Press a key.\n\nCurrent binding: " + u::gui::keyToString(m_key);
+       m_text->copy_label(tmp.c_str());
+}
+
+/* -------------------------------------------------------------------------- */
+
+int gdKeyGrabber::handle(int e)
+{
+       if (e != FL_KEYUP)
+               return Fl_Group::handle(e);
+
+       assert(onSetKey != nullptr);
+
+       const int newKey = Fl::event_key();
+
+       if (!onSetKey(newKey))
+       {
+               u::log::print("Invalid key\n");
+               return 1;
+       }
+
+       m_key = newKey;
+       rebuild();
+
+       u::log::print("Set key '%c' (%d)\n", m_key, m_key);
+
+       return 1;
+}
+} // namespace giada::v
diff --git a/src/gui/dialogs/keyGrabber.h b/src/gui/dialogs/keyGrabber.h
new file mode 100644 (file)
index 0000000..6c23efc
--- /dev/null
@@ -0,0 +1,67 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_KEYGRABBER_H
+#define GD_KEYGRABBER_H
+
+#include "core/conf.h"
+#include "window.h"
+#include <FL/Fl.H>
+#include <functional>
+
+namespace giada::c::channel
+{
+struct Data;
+}
+
+namespace giada::v
+{
+class geBox;
+class geButton;
+class gdKeyGrabber : public gdWindow
+{
+public:
+       gdKeyGrabber(int key);
+
+       int  handle(int e) override;
+       void rebuild() override;
+
+       /* onSetKey
+       Callback fired when this widget has grabbed an event. Returns a boolean value
+       to inform the widget if the key is valid. */
+
+       std::function<bool(int key)> onSetKey;
+
+private:
+       int m_key;
+
+       geBox*    m_text;
+       geButton* m_clear;
+       geButton* m_cancel;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/mainWindow.cpp b/src/gui/dialogs/mainWindow.cpp
new file mode 100644 (file)
index 0000000..c0c6582
--- /dev/null
@@ -0,0 +1,186 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "mainWindow.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/init.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/flex.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/mainWindow/mainIO.h"
+#include "gui/elems/mainWindow/mainMenu.h"
+#include "gui/elems/mainWindow/mainTimer.h"
+#include "gui/elems/mainWindow/mainTransport.h"
+#include "gui/elems/mainWindow/sequencer.h"
+#include "utils/gui.h"
+#include "warnings.h"
+#include <FL/Fl.H>
+#include <FL/Fl_Tooltip.H>
+
+namespace giada::v
+{
+gdMainWindow::ScopedProgress::ScopedProgress(gdProgress& p, const char* msg)
+: m_progress(p)
+{
+       m_progress.popup(msg);
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdMainWindow::ScopedProgress::~ScopedProgress()
+{
+       m_progress.hide();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdProgress& gdMainWindow::ScopedProgress::get()
+{
+       return m_progress;
+}
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+gdMainWindow::gdMainWindow(int W, int H, const char* title, int argc, char** argv, m::Conf::Data& c)
+: gdWindow(W, H, title)
+, m_conf(c)
+{
+       Fl::visible_focus(0);
+
+       Fl::background(25, 25, 25); // TODO use G_COLOR_GREY_1
+
+       Fl::set_boxtype(G_CUSTOM_BORDER_BOX, g_customBorderBox, 1, 1, 2, 2);
+       Fl::set_boxtype(G_CUSTOM_UP_BOX, g_customUpBox, 1, 1, 2, 2);
+       Fl::set_boxtype(G_CUSTOM_DOWN_BOX, g_customDownBox, 1, 1, 2, 2);
+
+       Fl::set_boxtype(FL_BORDER_BOX, G_CUSTOM_BORDER_BOX);
+       Fl::set_boxtype(FL_UP_BOX, G_CUSTOM_UP_BOX);
+       Fl::set_boxtype(FL_DOWN_BOX, G_CUSTOM_DOWN_BOX);
+
+       Fl_Tooltip::color(G_COLOR_GREY_1);
+       Fl_Tooltip::textcolor(G_COLOR_LIGHT_2);
+       Fl_Tooltip::size(G_GUI_FONT_SIZE_BASE);
+       Fl_Tooltip::enable(m_conf.showTooltips);
+
+       size_range(G_MIN_GUI_WIDTH, G_MIN_GUI_HEIGHT);
+
+       mainMenu      = new v::geMainMenu(0, 0);
+       mainIO        = new v::geMainIO(0, 0, 0, 0);
+       mainTransport = new v::geMainTransport(8, 39);
+       mainTimer     = new v::geMainTimer(571, 44);
+       sequencer     = new v::geSequencer(100, 78, 609, 30);
+       keyboard      = new v::geKeyboard(8, 122, w() - 16, 380);
+
+       /* zone 1 - menus, and I/O tools */
+
+       geFlex* zone1 = new geFlex(getContentBounds().reduced(G_GUI_OUTER_MARGIN).withH(G_GUI_UNIT),
+           Direction::HORIZONTAL, G_GUI_INNER_MARGIN);
+       zone1->add(mainMenu, 300);
+       zone1->add(new Fl_Box(0, 0, 0, 0));
+       zone1->add(mainIO, 430);
+       zone1->end();
+
+       /* zone 2 - mainTransport and timing tools */
+
+       Fl_Group* zone2 = new Fl_Group(8, mainTransport->y(), W - 16, mainTransport->h());
+       zone2->add(mainTransport);
+       zone2->resizable(new Fl_Box(mainTransport->x() + mainTransport->w() + 4, zone2->y(), 80, 20));
+       zone2->add(mainTimer);
+
+       /* zone 3 - beat meter */
+
+       Fl_Group* zone3 = new Fl_Group(8, sequencer->y(), W - 16, sequencer->h());
+       zone3->add(sequencer);
+
+       /* zone 4 - the keyboard (Fl_Group is unnecessary here, keyboard is
+        * a group by itself) */
+
+       resizable(keyboard);
+
+       add(zone1);
+       add(zone2);
+       add(zone3);
+       add(keyboard);
+
+       callback([](Fl_Widget* /*w*/, void* /*v*/) {
+               if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
+                       return; // ignore Escape
+               m::init::closeMainWindow();
+       });
+       u::gui::setFavicon(this);
+
+       refresh();
+
+       show(argc, argv);
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdMainWindow::~gdMainWindow()
+{
+       m_conf.mainWindowX = x();
+       m_conf.mainWindowY = y();
+       m_conf.mainWindowW = w();
+       m_conf.mainWindowH = h();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMainWindow::refresh()
+{
+       mainIO->refresh();
+       mainTimer->refresh();
+       mainTransport->refresh();
+       sequencer->refresh();
+       keyboard->refresh();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMainWindow::rebuild()
+{
+       keyboard->rebuild();
+       mainIO->rebuild();
+       mainTimer->rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMainWindow::clearKeyboard()
+{
+       keyboard->init();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdMainWindow::ScopedProgress gdMainWindow::getScopedProgress(const char* msg)
+{
+       return {m_progress, msg};
+}
+
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/mainWindow.h b/src/gui/dialogs/mainWindow.h
new file mode 100644 (file)
index 0000000..f0ae97d
--- /dev/null
@@ -0,0 +1,86 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MAINWINDOW_H
+#define GD_MAINWINDOW_H
+
+#include "core/conf.h"
+#include "gui/dialogs/progress.h"
+#include "window.h"
+
+namespace giada::v
+{
+class geKeyboard;
+class geMainIO;
+class geMainMenu;
+class geSequencer;
+class geMainTransport;
+class geMainTimer;
+class gdMainWindow : public gdWindow
+{
+       class ScopedProgress;
+
+public:
+       gdMainWindow(int w, int h, const char* title, int argc, char** argv, m::Conf::Data&);
+       ~gdMainWindow();
+
+       void refresh() override;
+       void rebuild() override;
+
+       /* clearKeyboard
+       Resets Keyboard to initial state, with no columns. */
+
+       void clearKeyboard();
+
+       [[nodiscard]] ScopedProgress getScopedProgress(const char* msg);
+
+       geKeyboard*      keyboard;
+       geSequencer*     sequencer;
+       geMainMenu*      mainMenu;
+       geMainIO*        mainIO;
+       geMainTimer*     mainTimer;
+       geMainTransport* mainTransport;
+
+private:
+       class ScopedProgress
+       {
+       public:
+               ScopedProgress(gdProgress&, const char* msg);
+               ~ScopedProgress();
+
+               gdProgress& get();
+
+       private:
+               gdProgress& m_progress;
+       };
+
+       m::Conf::Data& m_conf;
+
+       gdProgress m_progress;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/midiIO/midiInputBase.cpp b/src/gui/dialogs/midiIO/midiInputBase.cpp
new file mode 100644 (file)
index 0000000..c8189c0
--- /dev/null
@@ -0,0 +1,62 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiInputBase.h"
+#include "core/conf.h"
+#include "glue/io.h"
+
+namespace giada::v
+{
+gdMidiInputBase::gdMidiInputBase(int x, int y, int w, int h, const char* title,
+    m::Conf::Data& c)
+: gdWindow(x, y, w, h, title)
+, m_conf(c)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdMidiInputBase::~gdMidiInputBase()
+{
+       c::io::stopMidiLearn();
+
+       m_conf.midiInputX = x();
+       m_conf.midiInputY = y();
+       m_conf.midiInputW = w();
+       m_conf.midiInputH = h();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputBase::cb_close(Fl_Widget* /*w*/, void* p) { ((gdMidiInputBase*)p)->cb_close(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputBase::cb_close()
+{
+       do_callback();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/midiIO/midiInputBase.h b/src/gui/dialogs/midiIO/midiInputBase.h
new file mode 100644 (file)
index 0000000..6aa6cb3
--- /dev/null
@@ -0,0 +1,59 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MIDI_INPUT_BASE_H
+#define GD_MIDI_INPUT_BASE_H
+
+#include "core/conf.h"
+#include "gui/dialogs/window.h"
+#include "gui/elems/midiIO/midiLearner.h"
+
+class geCheck;
+
+namespace giada::v
+{
+class geButton;
+class geChoice;
+class gdMidiInputBase : public gdWindow
+{
+public:
+       virtual ~gdMidiInputBase();
+
+protected:
+       gdMidiInputBase(int x, int y, int w, int h, const char* title, m::Conf::Data&);
+
+       static void cb_close(Fl_Widget* /*w*/, void* p);
+       void        cb_close();
+
+       m::Conf::Data& m_conf;
+
+       geButton* m_ok;
+       geCheck*  m_enable;
+       geChoice* m_channel;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/midiIO/midiInputChannel.cpp b/src/gui/dialogs/midiIO/midiInputChannel.cpp
new file mode 100644 (file)
index 0000000..b41afff
--- /dev/null
@@ -0,0 +1,249 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/conf.h"
+#include "core/const.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include <FL/Fl_Pack.H>
+#include <cassert>
+#include <cstddef>
+#ifdef WITH_VST
+#include "core/plugins/plugin.h"
+#endif
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/choice.h"
+#include "gui/elems/basics/group.h"
+#include "gui/elems/basics/scrollPack.h"
+#include "gui/elems/midiIO/midiLearner.h"
+#include "gui/elems/midiIO/midiLearnerPack.h"
+#include "midiInputChannel.h"
+#include "utils/string.h"
+
+namespace giada::v
+{
+geChannelLearnerPack::geChannelLearnerPack(int x, int y, const c::io::Channel_InputData& channel)
+: geMidiLearnerPack(x, y, "Channel")
+{
+       setCallbacks(
+           [channelId = channel.channelId](int param) { c::io::channel_startMidiLearn(param, channelId); },
+           [channelId = channel.channelId](int param) { c::io::channel_clearMidiLearn(param, channelId); });
+       addMidiLearner("key press", G_MIDI_IN_KEYPRESS);
+       addMidiLearner("key release", G_MIDI_IN_KEYREL);
+       addMidiLearner("key kill", G_MIDI_IN_KILL);
+       addMidiLearner("arm", G_MIDI_IN_ARM);
+       addMidiLearner("mute", G_MIDI_IN_MUTE);
+       addMidiLearner("solo", G_MIDI_IN_SOLO);
+       addMidiLearner("volume", G_MIDI_IN_VOLUME);
+       addMidiLearner("pitch", G_MIDI_IN_PITCH, /*visible=*/channel.channelType == ChannelType::SAMPLE);
+       addMidiLearner("read actions", G_MIDI_IN_READ_ACTIONS, /*visible=*/channel.channelType == ChannelType::SAMPLE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelLearnerPack::update(const c::io::Channel_InputData& d)
+{
+       learners[0]->update(d.keyPress);
+       learners[1]->update(d.keyRelease);
+       learners[2]->update(d.kill);
+       learners[3]->update(d.arm);
+       learners[4]->update(d.mute);
+       learners[5]->update(d.solo);
+       learners[6]->update(d.volume);
+       learners[7]->update(d.pitch);
+       learners[8]->update(d.readActions);
+       setEnabled(d.enabled);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+gePluginLearnerPack::gePluginLearnerPack(int x, int y, const c::io::PluginData& plugin)
+: geMidiLearnerPack(x, y, plugin.name)
+{
+       setCallbacks(
+           [pluginId = plugin.id](int param) { c::io::plugin_startMidiLearn(param, pluginId); },
+           [pluginId = plugin.id](int param) { c::io::plugin_clearMidiLearn(param, pluginId); });
+
+       for (const c::io::PluginParamData& param : plugin.params)
+               addMidiLearner(param.name, param.index);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginLearnerPack::update(const c::io::PluginData& d, bool enabled)
+{
+       std::size_t i = 0;
+       for (const c::io::PluginParamData& param : d.params)
+               learners[i++]->update(param.value);
+       setEnabled(enabled);
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+gdMidiInputChannel::gdMidiInputChannel(ID channelId, m::Conf::Data& c)
+: gdMidiInputBase(c.midiInputX, c.midiInputY, c.midiInputW, c.midiInputH, "", c)
+, m_channelId(channelId)
+, m_data(c::io::channel_getInputData(channelId))
+{
+       end();
+
+       copy_label(std::string("MIDI Input Setup (channel " + std::to_string(channelId) + ")").c_str());
+
+       /* Header */
+
+       geGroup* groupHeader = new geGroup(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN);
+       m_enable             = new geCheck(0, 0, 120, G_GUI_UNIT, "Enable MIDI input");
+       m_channel            = new geChoice(m_enable->x() + m_enable->w() + 44, 0, 120, G_GUI_UNIT);
+       m_veloAsVol          = new geCheck(0, m_enable->y() + m_enable->h() + G_GUI_OUTER_MARGIN, w() - 16, G_GUI_UNIT,
+        "Velocity drives volume (Sample Channels)");
+       groupHeader->add(m_enable);
+       groupHeader->add(m_channel);
+       groupHeader->add(m_veloAsVol);
+       groupHeader->resizable(nullptr);
+
+       /* Main scrollable content. */
+
+       m_container = new geScrollPack(G_GUI_OUTER_MARGIN, groupHeader->y() + groupHeader->h() + G_GUI_OUTER_MARGIN,
+           w() - 16, h() - groupHeader->h() - 52);
+       m_container->add(new geChannelLearnerPack(0, 0, m_data));
+#ifdef WITH_VST
+       for (c::io::PluginData& plugin : m_data.plugins)
+               m_container->add(new gePluginLearnerPack(0, 0, plugin));
+#endif
+
+       /* Footer buttons. */
+
+       geGroup* groupButtons = new geGroup(G_GUI_OUTER_MARGIN, m_container->y() + m_container->h() + G_GUI_OUTER_MARGIN);
+       geBox*   spacer       = new geBox(0, 0, w() - 80, G_GUI_UNIT); // spacer window border <-> buttons
+       m_ok                  = new geButton(w() - 96, 0, 80, G_GUI_UNIT, "Close");
+       groupButtons->add(spacer);
+       groupButtons->add(m_ok);
+       groupButtons->resizable(spacer);
+
+       m_ok->callback(cb_close, (void*)this);
+       m_enable->callback(cb_enable, (void*)this);
+
+       m_channel->addItem("Channel (any)");
+       m_channel->addItem("Channel 1");
+       m_channel->addItem("Channel 2");
+       m_channel->addItem("Channel 3");
+       m_channel->addItem("Channel 4");
+       m_channel->addItem("Channel 5");
+       m_channel->addItem("Channel 6");
+       m_channel->addItem("Channel 7");
+       m_channel->addItem("Channel 8");
+       m_channel->addItem("Channel 9");
+       m_channel->addItem("Channel 10");
+       m_channel->addItem("Channel 11");
+       m_channel->addItem("Channel 12");
+       m_channel->addItem("Channel 13");
+       m_channel->addItem("Channel 14");
+       m_channel->addItem("Channel 15");
+       m_channel->addItem("Channel 16");
+       m_channel->onChange = [this](ID id) {
+               c::io::channel_setMidiInputFilter(m_data.channelId, id == 0 ? -1 : id - 1);
+       };
+
+       m_veloAsVol->callback(cb_veloAsVol, (void*)this);
+
+       add(groupHeader);
+       add(m_container);
+       add(groupButtons);
+       resizable(m_container);
+
+       u::gui::setFavicon(this);
+       set_modal();
+       rebuild();
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputChannel::rebuild()
+{
+       m_data = c::io::channel_getInputData(m_channelId);
+
+       m_enable->value(m_data.enabled);
+
+       if (m_data.channelType == ChannelType::SAMPLE)
+       {
+               m_veloAsVol->activate();
+               m_veloAsVol->value(m_data.velocityAsVol);
+       }
+       else
+               m_veloAsVol->deactivate();
+
+       int i = 0;
+       static_cast<geChannelLearnerPack*>(m_container->getChild(i++))->update(m_data);
+#ifdef WITH_VST
+       for (c::io::PluginData& plugin : m_data.plugins)
+               static_cast<gePluginLearnerPack*>(m_container->getChild(i++))->update(plugin, m_data.enabled);
+#endif
+
+       m_channel->showItem(m_data.filter == -1 ? 0 : m_data.filter + 1);
+
+       if (m_data.enabled)
+       {
+               m_channel->activate();
+               if (m_data.channelType == ChannelType::SAMPLE)
+                       m_veloAsVol->activate();
+       }
+       else
+       {
+               m_channel->deactivate();
+               m_veloAsVol->deactivate();
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputChannel::cb_enable(Fl_Widget* /*w*/, void* p) { ((gdMidiInputChannel*)p)->cb_enable(); }
+void gdMidiInputChannel::cb_veloAsVol(Fl_Widget* /*w*/, void* p) { ((gdMidiInputChannel*)p)->cb_veloAsVol(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputChannel::cb_enable()
+{
+       c::io::channel_enableMidiLearn(m_data.channelId, m_enable->value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputChannel::cb_veloAsVol()
+{
+       c::io::channel_enableVelocityAsVol(m_data.channelId, m_veloAsVol->value());
+}
+} // namespace giada::v
diff --git a/src/gui/dialogs/midiIO/midiInputChannel.h b/src/gui/dialogs/midiIO/midiInputChannel.h
new file mode 100644 (file)
index 0000000..142ebe3
--- /dev/null
@@ -0,0 +1,89 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * midiInputChannel
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MIDI_INPUT_CHANNEL_H
+#define GD_MIDI_INPUT_CHANNEL_H
+
+#include "core/conf.h"
+#include "glue/io.h"
+#include "gui/elems/midiIO/midiLearnerPack.h"
+#include "midiInputBase.h"
+
+class geCheck;
+
+namespace giada::v
+{
+class geChoice;
+class geScrollPack;
+class geChannelLearnerPack : public geMidiLearnerPack
+{
+public:
+       geChannelLearnerPack(int x, int y, const c::io::Channel_InputData& d);
+
+       void update(const c::io::Channel_InputData&);
+};
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+class gePluginLearnerPack : public geMidiLearnerPack
+{
+public:
+       gePluginLearnerPack(int x, int y, const c::io::PluginData&);
+
+       void update(const c::io::PluginData&, bool enabled);
+};
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+class gdMidiInputChannel : public gdMidiInputBase
+{
+public:
+       gdMidiInputChannel(ID channelId, m::Conf::Data&);
+
+       void rebuild() override;
+
+private:
+       static void cb_enable(Fl_Widget* /*w*/, void* p);
+       static void cb_veloAsVol(Fl_Widget* /*w*/, void* p);
+       void        cb_enable();
+       void        cb_veloAsVol();
+
+       ID m_channelId;
+
+       c::io::Channel_InputData m_data;
+
+       geScrollPack* m_container;
+       geCheck*      m_veloAsVol;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/midiIO/midiInputMaster.cpp b/src/gui/dialogs/midiIO/midiInputMaster.cpp
new file mode 100644 (file)
index 0000000..2a79e12
--- /dev/null
@@ -0,0 +1,151 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiInputMaster.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/choice.h"
+#include "gui/elems/basics/group.h"
+#include "gui/elems/basics/scrollPack.h"
+#include "gui/elems/midiIO/midiLearner.h"
+#include "utils/gui.h"
+#include <FL/Fl_Pack.H>
+
+namespace giada::v
+{
+geMasterLearnerPack::geMasterLearnerPack(int x, int y)
+: geMidiLearnerPack(x, y)
+{
+       setCallbacks(
+           [](int param) { c::io::master_startMidiLearn(param); },
+           [](int param) { c::io::master_clearMidiLearn(param); });
+       addMidiLearner("rewind", G_MIDI_IN_REWIND);
+       addMidiLearner("play/stop", G_MIDI_IN_START_STOP);
+       addMidiLearner("action recording", G_MIDI_IN_ACTION_REC);
+       addMidiLearner("input recording", G_MIDI_IN_INPUT_REC);
+       addMidiLearner("metronome", G_MIDI_IN_METRONOME);
+       addMidiLearner("input volume", G_MIDI_IN_VOLUME_IN);
+       addMidiLearner("output volume", G_MIDI_IN_VOLUME_OUT);
+       addMidiLearner("sequencer Ã—2", G_MIDI_IN_BEAT_DOUBLE);
+       addMidiLearner("sequencer Ã·2", G_MIDI_IN_BEAT_HALF);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMasterLearnerPack::update(const c::io::Master_InputData& d)
+{
+       learners[0]->update(d.rewind);
+       learners[1]->update(d.startStop);
+       learners[2]->update(d.actionRec);
+       learners[3]->update(d.inputRec);
+       learners[4]->update(d.metronome);
+       learners[5]->update(d.volumeIn);
+       learners[6]->update(d.volumeOut);
+       learners[7]->update(d.beatDouble);
+       learners[8]->update(d.beatHalf);
+       setEnabled(d.enabled);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+gdMidiInputMaster::gdMidiInputMaster(m::Conf::Data& c)
+: gdMidiInputBase(c.midiInputX, c.midiInputY, 300, 284, "MIDI Input Setup (global)", c)
+{
+       end();
+
+       geGroup* groupHeader = new geGroup(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN);
+       m_enable             = new geCheck(0, 0, 120, G_GUI_UNIT, "Enable MIDI input");
+       m_channel            = new geChoice(m_enable->x() + m_enable->w() + 44, 0, 120, G_GUI_UNIT);
+       groupHeader->resizable(nullptr);
+       groupHeader->add(m_enable);
+       groupHeader->add(m_channel);
+
+       m_learners = new geMasterLearnerPack(G_GUI_OUTER_MARGIN, groupHeader->y() + groupHeader->h() + G_GUI_OUTER_MARGIN);
+       m_ok       = new geButton(w() - 88, m_learners->y() + m_learners->h() + G_GUI_OUTER_MARGIN, 80, G_GUI_UNIT, "Close");
+
+       add(groupHeader);
+       add(m_learners);
+       add(m_ok);
+
+       m_ok->callback(cb_close, (void*)this);
+       m_enable->callback(cb_enable, (void*)this);
+
+       m_channel->addItem("Channel (any)");
+       m_channel->addItem("Channel 1");
+       m_channel->addItem("Channel 2");
+       m_channel->addItem("Channel 3");
+       m_channel->addItem("Channel 4");
+       m_channel->addItem("Channel 5");
+       m_channel->addItem("Channel 6");
+       m_channel->addItem("Channel 7");
+       m_channel->addItem("Channel 8");
+       m_channel->addItem("Channel 9");
+       m_channel->addItem("Channel 10");
+       m_channel->addItem("Channel 11");
+       m_channel->addItem("Channel 12");
+       m_channel->addItem("Channel 13");
+       m_channel->addItem("Channel 14");
+       m_channel->addItem("Channel 15");
+       m_channel->addItem("Channel 16");
+       m_channel->onChange = [](ID id) {
+               c::io::master_setMidiFilter(id == 0 ? -1 : id - 1);
+       };
+
+       u::gui::setFavicon(this);
+
+       set_modal();
+       rebuild();
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputMaster::rebuild()
+{
+       m_data = c::io::master_getInputData();
+
+       m_enable->value(m_data.enabled);
+       m_channel->showItem(m_data.filter - 1 ? 0 : m_data.filter + 1);
+       m_learners->update(m_data);
+
+       m_data.enabled ? m_channel->activate() : m_channel->deactivate();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputMaster::cb_enable(Fl_Widget* /*w*/, void* p) { ((gdMidiInputMaster*)p)->cb_enable(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiInputMaster::cb_enable()
+{
+       c::io::master_enableMidiLearn(m_enable->value());
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/midiIO/midiInputMaster.h b/src/gui/dialogs/midiIO/midiInputMaster.h
new file mode 100644 (file)
index 0000000..865236f
--- /dev/null
@@ -0,0 +1,67 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MIDI_INPUT_MASTER_H
+#define GD_MIDI_INPUT_MASTER_H
+
+#include "core/conf.h"
+#include "glue/io.h"
+#include "gui/elems/midiIO/midiLearnerPack.h"
+#include "midiInputBase.h"
+
+class geCheck;
+
+namespace giada::v
+{
+class geChoice;
+class geMasterLearnerPack : public geMidiLearnerPack
+{
+public:
+       geMasterLearnerPack(int x, int y);
+
+       void update(const c::io::Master_InputData&);
+};
+
+/* -------------------------------------------------------------------------- */
+
+class gdMidiInputMaster : public gdMidiInputBase
+{
+public:
+       gdMidiInputMaster(m::Conf::Data&);
+
+       void rebuild() override;
+
+private:
+       static void cb_enable(Fl_Widget* /*w*/, void* p);
+       void        cb_enable();
+
+       c::io::Master_InputData m_data;
+
+       geMasterLearnerPack* m_learners;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/midiIO/midiOutputBase.cpp b/src/gui/dialogs/midiIO/midiOutputBase.cpp
new file mode 100644 (file)
index 0000000..7635ab9
--- /dev/null
@@ -0,0 +1,101 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiOutputBase.h"
+#include "glue/io.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/midiIO/midiLearner.h"
+
+namespace giada
+{
+namespace v
+{
+geLightningLearnerPack::geLightningLearnerPack(int x, int y, ID channelId)
+: geMidiLearnerPack(x, y)
+{
+       setCallbacks(
+           [channelId](int param) { c::io::channel_startMidiLearn(param, channelId); },
+           [channelId](int param) { c::io::channel_clearMidiLearn(param, channelId); });
+       addMidiLearner("playing", G_MIDI_OUT_L_PLAYING);
+       addMidiLearner("mute", G_MIDI_OUT_L_MUTE);
+       addMidiLearner("solo", G_MIDI_OUT_L_SOLO);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geLightningLearnerPack::update(const c::io::Channel_OutputData& d)
+{
+       learners[0]->update(d.lightningPlaying);
+       learners[1]->update(d.lightningMute);
+       learners[2]->update(d.lightningSolo);
+       setEnabled(d.lightningEnabled);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+gdMidiOutputBase::gdMidiOutputBase(int w, int h, ID channelId)
+: gdWindow(w, h, "Midi Output Setup")
+, m_channelId(channelId)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdMidiOutputBase::~gdMidiOutputBase()
+{
+       c::io::stopMidiLearn();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiOutputBase::cb_close(Fl_Widget* /*w*/, void* p) { ((gdMidiOutputBase*)p)->cb_close(); }
+void gdMidiOutputBase::cb_enableLightning(Fl_Widget* /*w*/, void* p) { ((gdMidiOutputBase*)p)->cb_enableLightning(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiOutputBase::cb_close()
+{
+       do_callback();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiOutputBase::cb_enableLightning()
+{
+       c::io::channel_enableMidiLightning(m_channelId, m_enableLightning->value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiOutputBase::setTitle(ID channelId)
+{
+       std::string tmp = "MIDI Output Setup (channel " + std::to_string(channelId) + ")";
+       copy_label(tmp.c_str());
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/dialogs/midiIO/midiOutputBase.h b/src/gui/dialogs/midiIO/midiOutputBase.h
new file mode 100644 (file)
index 0000000..28b9e8f
--- /dev/null
@@ -0,0 +1,92 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MIDI_OUTPUT_BASE_H
+#define GD_MIDI_OUTPUT_BASE_H
+
+#include "core/types.h"
+#include "glue/io.h"
+#include "gui/dialogs/window.h"
+#include "gui/elems/midiIO/midiLearner.h"
+#include "gui/elems/midiIO/midiLearnerPack.h"
+
+class geCheck;
+
+/* There's no such thing as a gdMidiOutputMaster vs gdMidiOutputChannel. MIDI
+output master is managed by the configuration window, hence gdMidiOutput deals
+only with channels.
+
+Both MidiOutputMidiCh and MidiOutputSampleCh have the MIDI lighting widget set.
+In addition MidiOutputMidiCh has the MIDI message output box. */
+
+namespace giada
+{
+namespace v
+{
+class geButton;
+class geLightningLearnerPack : public geMidiLearnerPack
+{
+public:
+       geLightningLearnerPack(int x, int y, ID channelId);
+
+       void update(const c::io::Channel_OutputData&);
+};
+
+/* -------------------------------------------------------------------------- */
+
+class gdMidiOutputBase : public gdWindow
+{
+public:
+       gdMidiOutputBase(int w, int h, ID channelId);
+       ~gdMidiOutputBase();
+
+protected:
+       /* cb_close
+       close current window. */
+
+       static void cb_close(Fl_Widget* /*w*/, void* p);
+       void        cb_close();
+
+       static void cb_enableLightning(Fl_Widget* /*w*/, void* p);
+       void        cb_enableLightning();
+
+       /* setTitle
+        * set window title. */
+
+       void setTitle(ID channelId);
+
+       ID m_channelId;
+
+       c::io::Channel_OutputData m_data;
+
+       geLightningLearnerPack* m_learners;
+       geButton*               m_close;
+       geCheck*                m_enableLightning;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/dialogs/midiIO/midiOutputMidiCh.cpp b/src/gui/dialogs/midiIO/midiOutputMidiCh.cpp
new file mode 100644 (file)
index 0000000..ca31890
--- /dev/null
@@ -0,0 +1,118 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiOutputMidiCh.h"
+#include "glue/io.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/choice.h"
+#include "gui/elems/midiIO/midiLearner.h"
+#include "utils/gui.h"
+#include <FL/Fl_Pack.H>
+
+namespace giada::v
+{
+gdMidiOutputMidiCh::gdMidiOutputMidiCh(ID channelId)
+: gdMidiOutputBase(300, 168, channelId)
+{
+       end();
+       setTitle(m_channelId + 1);
+
+       m_enableOut   = new geCheck(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN, 150, G_GUI_UNIT, "Enable MIDI output");
+       m_chanListOut = new geChoice(w() - 108, G_GUI_OUTER_MARGIN, 100, G_GUI_UNIT);
+
+       m_enableLightning = new geCheck(G_GUI_OUTER_MARGIN, m_chanListOut->y() + m_chanListOut->h() + G_GUI_OUTER_MARGIN,
+           120, G_GUI_UNIT, "Enable MIDI lightning output");
+
+       m_learners = new geLightningLearnerPack(G_GUI_OUTER_MARGIN,
+           m_enableLightning->y() + m_enableLightning->h() + G_GUI_OUTER_MARGIN, channelId);
+
+       m_close = new geButton(w() - 88, m_learners->y() + m_learners->h() + G_GUI_OUTER_MARGIN, 80, G_GUI_UNIT, "Close");
+
+       add(m_enableOut);
+       add(m_chanListOut);
+       add(m_enableLightning);
+       add(m_learners);
+       add(m_close);
+
+       m_chanListOut->addItem("Channel 1");
+       m_chanListOut->addItem("Channel 2");
+       m_chanListOut->addItem("Channel 3");
+       m_chanListOut->addItem("Channel 4");
+       m_chanListOut->addItem("Channel 5");
+       m_chanListOut->addItem("Channel 6");
+       m_chanListOut->addItem("Channel 7");
+       m_chanListOut->addItem("Channel 8");
+       m_chanListOut->addItem("Channel 9");
+       m_chanListOut->addItem("Channel 10");
+       m_chanListOut->addItem("Channel 11");
+       m_chanListOut->addItem("Channel 12");
+       m_chanListOut->addItem("Channel 13");
+       m_chanListOut->addItem("Channel 14");
+       m_chanListOut->addItem("Channel 15");
+       m_chanListOut->addItem("Channel 16");
+       m_chanListOut->showItem(0);
+       m_chanListOut->onChange = [this](ID id) {
+               c::io::channel_setMidiOutputFilter(m_channelId, id);
+       };
+
+       m_enableOut->callback(cb_enableOut, (void*)this);
+       m_enableLightning->callback(cb_enableLightning, (void*)this);
+       m_close->callback(cb_close, (void*)this);
+
+       u::gui::setFavicon(this);
+
+       set_modal();
+       rebuild();
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiOutputMidiCh::rebuild()
+{
+       m_data = c::io::channel_getOutputData(m_channelId);
+
+       assert(m_data.output.has_value());
+
+       m_learners->update(m_data);
+       m_chanListOut->showItem(m_data.output->filter);
+       m_enableOut->value(m_data.output->enabled);
+
+       m_data.output->enabled ? m_chanListOut->activate() : m_chanListOut->deactivate();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiOutputMidiCh::cb_enableOut(Fl_Widget* /*w*/, void* p) { ((gdMidiOutputMidiCh*)p)->cb_enableOut(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiOutputMidiCh::cb_enableOut()
+{
+       c::io::channel_enableMidiOutput(m_channelId, m_enableOut->value());
+}
+} // namespace giada::v
diff --git a/src/gui/dialogs/midiIO/midiOutputMidiCh.h b/src/gui/dialogs/midiIO/midiOutputMidiCh.h
new file mode 100644 (file)
index 0000000..1abc715
--- /dev/null
@@ -0,0 +1,53 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * midiOutputMidiCh
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MIDI_OUTPUT_MIDI_CH_H
+#define GD_MIDI_OUTPUT_MIDI_CH_H
+
+#include "midiOutputBase.h"
+
+namespace giada::v
+{
+class geChoice;
+class gdMidiOutputMidiCh : public gdMidiOutputBase
+{
+public:
+       gdMidiOutputMidiCh(ID channelId);
+
+       void rebuild() override;
+
+private:
+       static void cb_enableOut(Fl_Widget* /*w*/, void* p);
+       void        cb_enableOut();
+
+       geCheck*  m_enableOut;
+       geChoice* m_chanListOut;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/midiIO/midiOutputSampleCh.cpp b/src/gui/dialogs/midiIO/midiOutputSampleCh.cpp
new file mode 100644 (file)
index 0000000..0c0fde3
--- /dev/null
@@ -0,0 +1,76 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiOutputSampleCh.h"
+#include "core/model/model.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/midiIO/midiLearner.h"
+#include "utils/gui.h"
+#include <FL/Fl_Pack.H>
+
+namespace giada
+{
+namespace v
+{
+gdMidiOutputSampleCh::gdMidiOutputSampleCh(ID channelId)
+: gdMidiOutputBase(300, 140, channelId)
+{
+       end();
+       setTitle(m_channelId);
+
+       m_enableLightning = new geCheck(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN, 120, 20, "Enable MIDI lightning output");
+
+       m_learners = new geLightningLearnerPack(G_GUI_OUTER_MARGIN,
+           m_enableLightning->y() + m_enableLightning->h() + 8, channelId);
+
+       m_close = new geButton(w() - 88, m_learners->y() + m_learners->h() + 8, 80, 20, "Close");
+
+       add(m_enableLightning);
+       add(m_learners);
+       add(m_close);
+
+       m_close->callback(cb_close, (void*)this);
+       m_enableLightning->callback(cb_enableLightning, (void*)this);
+
+       u::gui::setFavicon(this);
+
+       set_modal();
+       rebuild();
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdMidiOutputSampleCh::rebuild()
+{
+       m_data = c::io::channel_getOutputData(m_channelId);
+
+       m_enableLightning->value(m_data.lightningEnabled);
+       m_learners->update(m_data);
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/dialogs/midiIO/midiOutputSampleCh.h b/src/gui/dialogs/midiIO/midiOutputSampleCh.h
new file mode 100644 (file)
index 0000000..668a178
--- /dev/null
@@ -0,0 +1,46 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MIDI_OUTPUT_SAMPLE_CH_H
+#define GD_MIDI_OUTPUT_SAMPLE_CH_H
+
+#include "midiOutputBase.h"
+
+namespace giada
+{
+namespace v
+{
+class gdMidiOutputSampleCh : public gdMidiOutputBase
+{
+public:
+       gdMidiOutputSampleCh(ID channelId);
+
+       void rebuild() override;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/dialogs/missingAssets.cpp b/src/gui/dialogs/missingAssets.cpp
new file mode 100644 (file)
index 0000000..03b897f
--- /dev/null
@@ -0,0 +1,91 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/missingAssets.h"
+#include "core/engine.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/browser.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/flex.h"
+#include "utils/gui.h"
+#include <FL/Fl_Group.H>
+
+namespace giada::v
+{
+gdMissingAssets::gdMissingAssets(const m::LoadState& state)
+: gdWindow(u::gui::getCenterWinBounds(400, 300), "Warning")
+{
+       geFlex* container = new geFlex(getContentBounds().reduced({G_GUI_OUTER_MARGIN}), Direction::VERTICAL, G_GUI_OUTER_MARGIN);
+       {
+               geFlex* body = new geFlex(Direction::VERTICAL, G_GUI_INNER_MARGIN);
+               {
+                       geBox* textIntro = new geBox("This project contains missing assets.", FL_ALIGN_LEFT);
+                       textIntro->color(G_COLOR_BLUE);
+
+                       body->add(textIntro, G_GUI_UNIT);
+
+                       if (state.missingWaves.size() > 0)
+                       {
+                               geBrowser* waves = new geBrowser();
+                               for (const std::string& s : state.missingWaves)
+                                       waves->add(s.c_str());
+                               body->add(new geBox("Audio files not found in the project folder:", FL_ALIGN_LEFT), G_GUI_UNIT);
+                               body->add(waves);
+                       }
+
+                       if (state.missingPlugins.size() > 0)
+                       {
+                               geBrowser* plugins = new geBrowser();
+                               for (const std::string& s : state.missingPlugins)
+                                       plugins->add(s.c_str());
+                               body->add(new geBox("Audio plug-ins not found globally:", FL_ALIGN_LEFT), G_GUI_UNIT);
+                               body->add(plugins);
+                       }
+                       body->end();
+               }
+
+               geFlex* footer = new geFlex(Direction::HORIZONTAL);
+               {
+                       geButton* close = new geButton("Close");
+                       close->onClick  = [this]() { do_callback(); };
+                       footer->add(new geBox()); // Spacer
+                       footer->add(close, 80);
+                       footer->end();
+               }
+
+               container->add(body);
+               container->add(footer, G_GUI_UNIT);
+               container->end();
+       }
+
+       add(container);
+       resizable(container);
+
+       set_modal();
+       u::gui::setFavicon(this);
+       show();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/missingAssets.h b/src/gui/dialogs/missingAssets.h
new file mode 100644 (file)
index 0000000..bd1ca05
--- /dev/null
@@ -0,0 +1,46 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_MISSING_ASSETS_H
+#define GD_MISSING_ASSETS_H
+
+#include "gui/dialogs/window.h"
+
+namespace giada::m
+{
+struct LoadState;
+}
+
+namespace giada::v
+{
+class gdMissingAssets : public gdWindow
+{
+public:
+       gdMissingAssets(const m::LoadState&);
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/pluginChooser.cpp b/src/gui/dialogs/pluginChooser.cpp
new file mode 100644 (file)
index 0000000..1f06e17
--- /dev/null
@@ -0,0 +1,121 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "pluginChooser.h"
+#include "core/conf.h"
+#include "glue/plugin.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/choice.h"
+#include "gui/elems/plugin/pluginBrowser.h"
+#include "utils/gui.h"
+
+namespace giada::v
+{
+gdPluginChooser::gdPluginChooser(int X, int Y, int W, int H, ID channelId, m::Conf::Data& c)
+: gdWindow(X, Y, W, H, "Available plugins")
+, m_conf(c)
+, m_channelId(channelId)
+{
+       begin();
+
+       /* top area */
+       Fl_Group* group_top = new Fl_Group(8, 8, w() - 16, 20);
+       sortMethod          = new geChoice(group_top->x(), group_top->y(), 180, 20, "Sort by", 0);
+       geBox* b1           = new geBox(sortMethod->x() + sortMethod->w(), group_top->y(), 100, 20); // spacer window border <-> menu
+       group_top->resizable(b1);
+       group_top->end();
+
+       /* center browser */
+       browser = new v::gePluginBrowser(8, 36, w() - 16, h() - 70);
+
+       /* ok/cancel buttons */
+       Fl_Group* group_btn = new Fl_Group(8, browser->y() + browser->h() + 8, w() - 16, h() - browser->h() - 16);
+       geBox*    b2        = new geBox(8, browser->y() + browser->h(), 100, 20); // spacer window border <-> buttons
+       addBtn              = new geButton(w() - 88, group_btn->y(), 80, 20, "Add");
+       cancelBtn           = new geButton(addBtn->x() - 88, group_btn->y(), 80, 20, "Cancel");
+       group_btn->resizable(b2);
+       group_btn->end();
+
+       end();
+
+       sortMethod->addItem("Name");
+       sortMethod->addItem("Category");
+       sortMethod->addItem("Manufacturer");
+       sortMethod->addItem("Format");
+       sortMethod->showItem(m_conf.pluginSortMethod);
+       sortMethod->onChange = [this](ID id) {
+               c::plugin::sortPlugins(static_cast<m::PluginManager::SortMethod>(id));
+               browser->refresh();
+       };
+
+       addBtn->callback(cb_add, (void*)this);
+       addBtn->shortcut(FL_Enter);
+       cancelBtn->callback(cb_close, (void*)this);
+
+       resizable(browser);
+       u::gui::setFavicon(this);
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdPluginChooser::~gdPluginChooser()
+{
+       m_conf.pluginChooserX   = x();
+       m_conf.pluginChooserY   = y();
+       m_conf.pluginChooserW   = w();
+       m_conf.pluginChooserH   = h();
+       m_conf.pluginSortMethod = sortMethod->getSelectedId();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginChooser::cb_close(Fl_Widget* /*w*/, void* p) { ((gdPluginChooser*)p)->cb_close(); }
+void gdPluginChooser::cb_add(Fl_Widget* /*w*/, void* p) { ((gdPluginChooser*)p)->cb_add(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginChooser::cb_close()
+{
+       do_callback();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginChooser::cb_add()
+{
+       int pluginIndex = browser->value() - 3; // subtract header lines
+       if (pluginIndex < 0)
+               return;
+       c::plugin::addPlugin(pluginIndex, m_channelId);
+       do_callback();
+}
+} // namespace giada::v
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/dialogs/pluginChooser.h b/src/gui/dialogs/pluginChooser.h
new file mode 100644 (file)
index 0000000..80d4389
--- /dev/null
@@ -0,0 +1,69 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef GD_PLUGIN_CHOOSER_H
+#define GD_PLUGIN_CHOOSER_H
+
+#include "core/conf.h"
+#include "core/types.h"
+#include "window.h"
+#include <FL/Fl.H>
+#include <FL/Fl_Scroll.H>
+
+namespace giada::v
+{
+class geButton;
+class geChoice;
+class gePluginBrowser;
+
+class gdPluginChooser : public gdWindow
+{
+public:
+       gdPluginChooser(int x, int y, int w, int h, ID channelId, m::Conf::Data&);
+       ~gdPluginChooser();
+
+private:
+       static void cb_close(Fl_Widget* /*w*/, void* p);
+       static void cb_add(Fl_Widget* /*w*/, void* p);
+       void        cb_close();
+       void        cb_add();
+
+       m::Conf::Data& m_conf;
+
+       geChoice*        sortMethod;
+       geButton*        addBtn;
+       geButton*        cancelBtn;
+       gePluginBrowser* browser;
+
+       ID m_channelId;
+};
+} // namespace giada::v
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/dialogs/pluginList.cpp b/src/gui/dialogs/pluginList.cpp
new file mode 100644 (file)
index 0000000..02fee22
--- /dev/null
@@ -0,0 +1,136 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "gui/dialogs/pluginList.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "glue/layout.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/liquidScroll.h"
+#include "gui/elems/basics/statusButton.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/mainWindow/mainIO.h"
+#include "gui/elems/plugin/pluginElement.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <cassert>
+#include <string>
+
+namespace giada::v
+{
+gdPluginList::gdPluginList(ID channelId, m::Conf::Data& c)
+: gdWindow(c.pluginListX, c.pluginListY, 468, 204)
+, m_conf(c)
+, m_channelId(channelId)
+{
+       end();
+
+       list = new geLiquidScroll(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN,
+           w() - (G_GUI_OUTER_MARGIN * 2), h() - (G_GUI_OUTER_MARGIN * 2),
+           Direction::VERTICAL);
+       list->end();
+       add(list);
+       resizable(list);
+
+       u::gui::setFavicon(this);
+       set_non_modal();
+       rebuild();
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdPluginList::~gdPluginList()
+{
+       m_conf.pluginListX = x();
+       m_conf.pluginListY = y();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginList::cb_addPlugin(Fl_Widget* /*v*/, void* p) { ((gdPluginList*)p)->cb_addPlugin(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginList::rebuild()
+{
+       m_plugins = c::plugin::getPlugins(m_channelId);
+
+       if (m_plugins.channelId == m::Mixer::MASTER_OUT_CHANNEL_ID)
+               label("Master Out Plug-ins");
+       else if (m_plugins.channelId == m::Mixer::MASTER_IN_CHANNEL_ID)
+               label("Master In Plug-ins");
+       else
+       {
+               std::string l = "Channel " + u::string::iToString(m_plugins.channelId) + " Plug-ins";
+               copy_label(l.c_str());
+       }
+
+       /* Clear the previous list. */
+
+       list->clear();
+       list->scroll_to(0, 0);
+
+       for (m::Plugin* plugin : m_plugins.plugins)
+               list->addWidget(new gePluginElement(0, 0, c::plugin::getPlugin(*plugin, m_plugins.channelId)));
+
+       addPlugin = list->addWidget(new geButton(0, 0, 0, G_GUI_UNIT, "-- add new plugin --"));
+
+       addPlugin->callback(cb_addPlugin, (void*)this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginList::cb_addPlugin()
+{
+       c::layout::openPluginChooser(m_plugins.channelId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+const gePluginElement& gdPluginList::getNextElement(const gePluginElement& currEl) const
+{
+       int curr = list->find(currEl);
+       int next = curr + 1;
+       if (next > list->countChildren() - 2)
+               next = list->countChildren() - 2;
+       return *static_cast<gePluginElement*>(list->child(next));
+}
+
+const gePluginElement& gdPluginList::getPrevElement(const gePluginElement& currEl) const
+{
+       int curr = list->find(currEl);
+       int prev = curr - 1;
+       if (prev < 0)
+               prev = 0;
+       return *static_cast<gePluginElement*>(list->child(prev));
+}
+} // namespace giada::v
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/dialogs/pluginList.h b/src/gui/dialogs/pluginList.h
new file mode 100644 (file)
index 0000000..81d043c
--- /dev/null
@@ -0,0 +1,68 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef GD_PLUGINLIST_H
+#define GD_PLUGINLIST_H
+
+#include "core/conf.h"
+#include "glue/plugin.h"
+#include "window.h"
+
+namespace giada::v
+{
+class geButton;
+class geLiquidScroll;
+class gePluginElement;
+class gdPluginList : public gdWindow
+{
+public:
+       gdPluginList(ID channelId, m::Conf::Data&);
+       ~gdPluginList();
+
+       void rebuild() override;
+
+       const gePluginElement& getNextElement(const gePluginElement& curr) const;
+       const gePluginElement& getPrevElement(const gePluginElement& curr) const;
+
+private:
+       static void cb_addPlugin(Fl_Widget* /*w*/, void* p);
+       void        cb_addPlugin();
+
+       m::Conf::Data& m_conf;
+
+       geButton*       addPlugin;
+       geLiquidScroll* list;
+
+       ID                 m_channelId;
+       c::plugin::Plugins m_plugins;
+};
+} // namespace giada::v
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/dialogs/pluginWindow.cpp b/src/gui/dialogs/pluginWindow.cpp
new file mode 100644 (file)
index 0000000..f4e712a
--- /dev/null
@@ -0,0 +1,82 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "pluginWindow.h"
+#include "core/const.h"
+#include "glue/plugin.h"
+#include "gui/elems/basics/liquidScroll.h"
+#include "gui/elems/plugin/pluginParameter.h"
+#include "utils/gui.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+gdPluginWindow::gdPluginWindow(const c::plugin::Plugin& plugin)
+: gdWindow(450, 156)
+, m_plugin(plugin)
+{
+       set_non_modal();
+
+       begin();
+
+       m_list = new geLiquidScroll(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN,
+           w() - (G_GUI_OUTER_MARGIN * 2), h() - (G_GUI_OUTER_MARGIN * 2),
+           Direction::VERTICAL);
+
+       m_list->type(Fl_Scroll::VERTICAL_ALWAYS);
+       m_list->begin();
+       int labelWidth = 100; // TODO
+       for (int index : m_plugin.paramIndexes)
+       {
+               int py = m_list->y() + (index * (G_GUI_UNIT + G_GUI_INNER_MARGIN));
+               int pw = m_list->w() - m_list->scrollbar_size() - (G_GUI_OUTER_MARGIN * 3);
+               new v::gePluginParameter(m_list->x(), py, pw, labelWidth, c::plugin::getParam(index, m_plugin.getPluginRef(), m_plugin.channelId));
+       }
+       m_list->end();
+
+       end();
+
+       label(m_plugin.name.c_str());
+
+       size_range(450, (G_GUI_UNIT + (G_GUI_OUTER_MARGIN * 2)));
+       resizable(m_list);
+
+       u::gui::setFavicon(this);
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginWindow::updateParameters(bool changeSlider)
+{
+       for (int index : m_plugin.paramIndexes)
+               static_cast<v::gePluginParameter*>(m_list->child(index))->update(c::plugin::getParam(index, m_plugin.getPluginRef(), m_plugin.channelId), changeSlider);
+}
+} // namespace giada::v
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/dialogs/pluginWindow.h b/src/gui/dialogs/pluginWindow.h
new file mode 100644 (file)
index 0000000..875d821
--- /dev/null
@@ -0,0 +1,66 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef GD_PLUGIN_WINDOW_H
+#define GD_PLUGIN_WINDOW_H
+
+#include "window.h"
+
+class geSlider;
+
+namespace giada::c::plugin
+{
+struct Plugin;
+}
+
+namespace giada::m
+{
+class Plugin;
+}
+
+namespace giada::v
+{
+class geBox;
+class geLiquidScroll;
+class gdPluginWindow : public gdWindow
+{
+public:
+       gdPluginWindow(const c::plugin::Plugin&);
+
+       void updateParameters(bool changeSlider = false);
+
+private:
+       const c::plugin::Plugin& m_plugin;
+
+       geLiquidScroll* m_list;
+};
+} // namespace giada::v
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/dialogs/pluginWindowGUI.cpp b/src/gui/dialogs/pluginWindowGUI.cpp
new file mode 100644 (file)
index 0000000..5568c57
--- /dev/null
@@ -0,0 +1,115 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "gui/dialogs/pluginWindowGUI.h"
+#include "core/const.h"
+#include "glue/plugin.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include <FL/x.H>
+#ifdef G_OS_MAC
+#import "utils/cocoa.h" // objective-c
+#endif
+
+namespace giada::v
+{
+gdPluginWindowGUI::gdPluginWindowGUI(c::plugin::Plugin& p)
+#ifdef G_OS_MAC
+: gdWindow(Fl::w(), Fl::h())
+#else
+: gdWindow(320, 200)
+#endif
+, m_plugin(p)
+{
+       /* Make sure to wait_for_expose() before opening the editor: the window must
+       be exposed and visible first. Don't fuck with multithreading! */
+
+       copy_label(m_plugin.name.c_str());
+       show();
+       wait_for_expose();
+       openEditor();
+       Fl::flush();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdPluginWindowGUI::~gdPluginWindowGUI()
+{
+       c::plugin::stopDispatchLoop();
+       closeEditor();
+       u::log::print("[gdPluginWindowGUI::__cb_close] GUI closed, this=%p\n", (void*)this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginWindowGUI::openEditor()
+{
+       u::log::print("[gdPluginWindowGUI] Opening editor, this=%p, xid=%p\n",
+           this, reinterpret_cast<void*>(fl_xid(this)));
+
+       m_editor.reset(m_plugin.createEditor());
+       if (m_editor == nullptr)
+       {
+               u::log::print("[gdPluginWindowGUI::openEditor] unable to create editor!\n");
+               return;
+       }
+       m_editor->setOpaque(true);
+
+#ifdef G_OS_MAC
+
+       void* cocoaWindow = (void*)fl_xid(this);
+       m_editor->addToDesktop(0, cocoa_getViewFromWindow(cocoaWindow));
+
+#else
+
+       m_editor->addToDesktop(0, reinterpret_cast<void*>(fl_xid(this)));
+
+#endif
+
+       const int pluginW = m_editor->getWidth();
+       const int pluginH = m_editor->getHeight();
+
+       resize((Fl::w() - pluginW) / 2, (Fl::h() - pluginH) / 2, pluginW, pluginH);
+
+       m_plugin.setResizeCallback([this](int w, int h) {
+               resize(x(), y(), w, h);
+       });
+
+       c::plugin::startDispatchLoop();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdPluginWindowGUI::closeEditor()
+{
+       m_plugin.setResizeCallback(nullptr);
+       m_editor.reset();
+}
+} // namespace giada::v
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/dialogs/pluginWindowGUI.h b/src/gui/dialogs/pluginWindowGUI.h
new file mode 100644 (file)
index 0000000..6877c16
--- /dev/null
@@ -0,0 +1,65 @@
+
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * gd_pluginWindowGUI
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef GD_PLUGIN_WINDOW_GUI_H
+#define GD_PLUGIN_WINDOW_GUI_H
+
+#include "deps/juce-config.h"
+#include "window.h"
+#include <FL/Fl.H>
+#include <FL/Fl_Window.H>
+#include <memory>
+
+namespace giada::c::plugin
+{
+struct Plugin;
+}
+
+namespace giada::v
+{
+class gdPluginWindowGUI : public gdWindow
+{
+public:
+       gdPluginWindowGUI(c::plugin::Plugin&);
+       ~gdPluginWindowGUI();
+
+       void openEditor();
+       void closeEditor();
+
+private:
+       c::plugin::Plugin&                          m_plugin;
+       std::unique_ptr<juce::AudioProcessorEditor> m_editor;
+};
+} // namespace giada::v
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/dialogs/progress.cpp b/src/gui/dialogs/progress.cpp
new file mode 100644 (file)
index 0000000..a42cb3d
--- /dev/null
@@ -0,0 +1,76 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/progress.h"
+#include "core/const.h"
+#include "deps/geompp/src/rect.hpp"
+#include "utils/gui.h"
+#include <FL/Fl.H>
+
+namespace giada::v
+{
+gdProgress::gdProgress()
+: gdWindow(300, 58)
+, m_text(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN, w() - (G_GUI_OUTER_MARGIN * 2), 30, "", FL_ALIGN_CENTER)
+, m_progress(G_GUI_OUTER_MARGIN, 40, w() - (G_GUI_OUTER_MARGIN * 2), 10)
+{
+       end();
+       add(m_text);
+       add(m_progress);
+
+       m_progress.minimum(0.0f);
+       m_progress.maximum(1.0f);
+       m_progress.value(0.0f);
+
+       hide();
+       border(0);
+       set_modal();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdProgress::setProgress(float p)
+{
+       m_progress.value(p);
+       redraw();
+       Fl::flush();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdProgress::popup(const char* s)
+{
+       m_text.copy_label(s);
+
+       const int px = u::gui::centerWindowX(w());
+       const int py = u::gui::centerWindowY(h());
+
+       position(px, py);
+       show();
+       wait_for_expose(); // No async bullshit, show it right away
+       Fl::flush();       // Make sure everything is displayed
+}
+} // namespace giada::v
diff --git a/src/gui/dialogs/progress.h b/src/gui/dialogs/progress.h
new file mode 100644 (file)
index 0000000..d9b6759
--- /dev/null
@@ -0,0 +1,50 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_PROGRESS_H
+#define GD_PROGRESS_H
+
+#include "gui/dialogs/window.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/progress.h"
+
+namespace giada::v
+{
+class gdProgress : public gdWindow
+{
+public:
+       gdProgress();
+
+       void setProgress(float p);
+       void popup(const char* s);
+
+private:
+       geBox      m_text;
+       geProgress m_progress;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/sampleEditor.cpp b/src/gui/dialogs/sampleEditor.cpp
new file mode 100644 (file)
index 0000000..194748f
--- /dev/null
@@ -0,0 +1,331 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "glue/sampleEditor.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "core/mixer.h"
+#include "core/wave.h"
+#include "core/waveFx.h"
+#include "glue/channel.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/choice.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/group.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/basics/pack.h"
+#include "gui/elems/basics/statusButton.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/sampleEditor/boostTool.h"
+#include "gui/elems/sampleEditor/panTool.h"
+#include "gui/elems/sampleEditor/pitchTool.h"
+#include "gui/elems/sampleEditor/rangeTool.h"
+#include "gui/elems/sampleEditor/shiftTool.h"
+#include "gui/elems/sampleEditor/volumeTool.h"
+#include "gui/elems/sampleEditor/waveTools.h"
+#include "gui/elems/sampleEditor/waveform.h"
+#include "sampleEditor.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <FL/Fl.H>
+#include <FL/Fl_Group.H>
+#include <cassert>
+#include <cmath>
+
+#ifdef G_OS_WINDOWS
+#undef IN
+#undef OUT
+#endif
+
+namespace giada::v
+{
+gdSampleEditor::gdSampleEditor(ID channelId, m::Conf::Data& c)
+: gdWindow(c.sampleEditorX, c.sampleEditorY, c.sampleEditorW, c.sampleEditorH)
+, m_channelId(channelId)
+, m_conf(c)
+{
+       end();
+
+       gePack* upperBar = createUpperBar();
+
+       waveTools = new geWaveTools(G_GUI_OUTER_MARGIN, upperBar->y() + upperBar->h() + G_GUI_OUTER_MARGIN,
+           w() - 16, h() - 168, m_conf.sampleEditorGridOn, m_conf.sampleEditorGridVal);
+
+       gePack* bottomBar = createBottomBar(G_GUI_OUTER_MARGIN, waveTools->y() + waveTools->h() + G_GUI_OUTER_MARGIN,
+           h() - waveTools->h() - upperBar->h() - 32);
+
+       add(upperBar);
+       add(waveTools);
+       add(bottomBar);
+
+       resizable(waveTools);
+
+       u::gui::setFavicon(this);
+
+       size_range(720, 480);
+       set_non_modal();
+       rebuild();
+       show();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdSampleEditor::~gdSampleEditor()
+{
+       m_conf.sampleEditorX       = x();
+       m_conf.sampleEditorY       = y();
+       m_conf.sampleEditorW       = w();
+       m_conf.sampleEditorH       = h();
+       m_conf.sampleEditorGridVal = grid->getSelectedId();
+       m_conf.sampleEditorGridOn  = snap->value();
+
+       c::sampleEditor::stopPreview();
+       c::sampleEditor::cleanupPreview();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::rebuild()
+{
+       m_data = c::sampleEditor::getData(m_channelId);
+
+       copy_label(m_data.name.c_str());
+
+       waveTools->rebuild(m_data);
+       volumeTool->rebuild(m_data);
+       panTool->rebuild(m_data);
+       pitchTool->rebuild(m_data);
+       rangeTool->rebuild(m_data);
+       shiftTool->rebuild(m_data);
+
+       updateInfo();
+
+       if (m_data.isLogical) // Logical samples (aka takes) cannot be reloaded.
+               reload->deactivate();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::refresh()
+{
+       waveTools->refresh();
+       play->setStatus(m_data.a_getPreviewStatus() == ChannelStatus::PLAY);
+}
+
+/* -------------------------------------------------------------------------- */
+
+gePack* gdSampleEditor::createUpperBar()
+{
+       reload  = new geButton(0, 0, 70, G_GUI_UNIT, "Reload");
+       grid    = new geChoice(0, 0, 50, G_GUI_UNIT);
+       snap    = new geCheck(0, 0, 12, G_GUI_UNIT, "Snap");
+       sep1    = new geBox(0, 0, w() - 208, G_GUI_UNIT);
+       zoomOut = new geButton(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", zoomOutOff_xpm, zoomOutOn_xpm);
+       zoomIn  = new geButton(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", zoomInOff_xpm, zoomInOn_xpm);
+
+       reload->callback(cb_reload, (void*)this);
+
+       grid->addItem("(off)");
+       grid->addItem("2");
+       grid->addItem("3");
+       grid->addItem("4");
+       grid->addItem("6");
+       grid->addItem("8");
+       grid->addItem("16");
+       grid->addItem("32");
+       grid->addItem("64");
+       grid->copy_tooltip("Grid frequency");
+       grid->showItem(m_conf.sampleEditorGridVal);
+       grid->onChange = [this](ID) {
+               waveTools->waveform->setGridLevel(std::stoi(grid->getSelectedLabel()));
+       };
+
+       snap->value(m_conf.sampleEditorGridOn);
+       snap->copy_tooltip("Snap to grid");
+       snap->callback(cb_enableSnap, (void*)this);
+
+       /* TODO - redraw grid if != (off) */
+
+       zoomOut->callback(cb_zoomOut, (void*)this);
+       zoomOut->copy_tooltip("Zoom out");
+       zoomIn->callback(cb_zoomIn, (void*)this);
+       zoomIn->copy_tooltip("Zoom in");
+
+       gePack* g = new gePack(G_GUI_OUTER_MARGIN, G_GUI_OUTER_MARGIN, Direction::HORIZONTAL);
+       g->add(reload);
+       g->add(grid);
+       g->add(snap);
+       g->add(sep1);
+       g->add(zoomOut);
+       g->add(zoomIn);
+       g->resizable(sep1);
+
+       return g;
+}
+
+/* -------------------------------------------------------------------------- */
+
+gePack* gdSampleEditor::createOpTools(int x, int y)
+{
+       volumeTool = new geVolumeTool(m_data, 0, 0);
+       panTool    = new gePanTool(m_data, 0, 0);
+       pitchTool  = new gePitchTool(m_data, 0, 0);
+       rangeTool  = new geRangeTool(m_data, 0, 0);
+       shiftTool  = new geShiftTool(m_data, 0, 0);
+
+       gePack* g = new gePack(x, y, Direction::VERTICAL);
+       g->add(volumeTool);
+       g->add(panTool);
+       g->add(pitchTool);
+       g->add(rangeTool);
+       g->add(shiftTool);
+
+       return g;
+}
+
+/* -------------------------------------------------------------------------- */
+
+geGroup* gdSampleEditor::createPreviewBox(int x, int y, int h)
+{
+       rewind = new geButton(x, y + (h / 2) - 12, 25, 25, "", rewindOff_xpm, rewindOn_xpm);
+       play   = new geStatusButton(rewind->x() + rewind->w() + 4, rewind->y(), 25, 25, play_xpm, pause_xpm);
+       loop   = new geCheck(play->x() + play->w() + 4, play->y(), 50, 25, "Loop");
+
+       play->callback(cb_togglePreview, (void*)this);
+       rewind->callback(cb_rewindPreview, (void*)this);
+
+       geGroup* g = new geGroup(x, y);
+       g->add(rewind);
+       g->add(play);
+       g->add(loop);
+
+       return g;
+}
+
+/* -------------------------------------------------------------------------- */
+
+gePack* gdSampleEditor::createBottomBar(int x, int y, int h)
+{
+       geGroup*  previewBox = createPreviewBox(0, 0, h);
+       geBox*    divisor1   = new geBox(0, 0, 1, h);
+       Fl_Group* opTools    = createOpTools(0, 0);
+       geBox*    divisor2   = new geBox(0, 0, 1, h);
+       info                 = new geBox(0, 0, 400, h);
+
+       divisor1->box(FL_BORDER_BOX);
+       divisor2->box(FL_BORDER_BOX);
+
+       info->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_TOP);
+
+       gePack* g = new gePack(x, y, Direction::HORIZONTAL, /*gutter=*/G_GUI_OUTER_MARGIN);
+       g->add(previewBox);
+       g->add(divisor1);
+       g->add(opTools);
+       g->add(divisor2);
+       g->add(info);
+       g->resizable(0);
+
+       return g;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::cb_reload(Fl_Widget* /*w*/, void* p) { ((gdSampleEditor*)p)->cb_reload(); }
+void gdSampleEditor::cb_zoomIn(Fl_Widget* /*w*/, void* p) { ((gdSampleEditor*)p)->cb_zoomIn(); }
+void gdSampleEditor::cb_zoomOut(Fl_Widget* /*w*/, void* p) { ((gdSampleEditor*)p)->cb_zoomOut(); }
+void gdSampleEditor::cb_enableSnap(Fl_Widget* /*w*/, void* p) { ((gdSampleEditor*)p)->cb_enableSnap(); }
+void gdSampleEditor::cb_togglePreview(Fl_Widget* /*w*/, void* p) { ((gdSampleEditor*)p)->cb_togglePreview(); }
+void gdSampleEditor::cb_rewindPreview(Fl_Widget* /*w*/, void* p) { ((gdSampleEditor*)p)->cb_rewindPreview(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::cb_enableSnap()
+{
+       waveTools->waveform->setSnap(!waveTools->waveform->getSnap());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::cb_togglePreview()
+{
+       if (!play->getStatus())
+               c::sampleEditor::playPreview(loop->value());
+       else
+               c::sampleEditor::stopPreview();
+}
+
+void gdSampleEditor::cb_rewindPreview()
+{
+       c::sampleEditor::setPreviewTracker(m_data.begin);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::cb_reload()
+{
+       c::sampleEditor::reload(m_data.channelId);
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::cb_zoomIn()
+{
+       waveTools->waveform->setZoom(geWaveform::Zoom::IN);
+       waveTools->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::cb_zoomOut()
+{
+       waveTools->waveform->setZoom(geWaveform::Zoom::OUT);
+       waveTools->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdSampleEditor::updateInfo()
+{
+       std::string bitDepth = m_data.waveBits != 0 ? u::string::iToString(m_data.waveBits) : "(unknown)";
+       std::string infoText =
+           "File: " + m_data.wavePath + "\n"
+                                        "Size: " +
+           u::string::iToString(m_data.waveSize) + " frames\n"
+                                                   "Duration: " +
+           u::string::iToString(m_data.waveDuration) + " seconds\n"
+                                                       "Bit depth: " +
+           bitDepth + "\n"
+                      "Frequency: " +
+           u::string::iToString(m_data.waveRate) + " Hz\n";
+
+       info->copy_label(infoText.c_str());
+}
+} // namespace giada::v
diff --git a/src/gui/dialogs/sampleEditor.h b/src/gui/dialogs/sampleEditor.h
new file mode 100644 (file)
index 0000000..fa48b03
--- /dev/null
@@ -0,0 +1,118 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_EDITOR_H
+#define GD_EDITOR_H
+
+#include "core/conf.h"
+#include "core/types.h"
+#include "glue/sampleEditor.h"
+#include "window.h"
+
+class geCheck;
+class geStatusButton;
+
+namespace giada::m
+{
+class Wave;
+}
+
+namespace giada::v
+{
+class geBox;
+class geButton;
+class geChoice;
+class gePack;
+class geGroup;
+class geVolumeTool;
+class geWaveTools;
+class geBoostTool;
+class gePanTool;
+class gePitchTool;
+class geRangeTool;
+class geShiftTool;
+class gdSampleEditor : public gdWindow
+{
+       friend class geWaveform;
+
+public:
+       gdSampleEditor(ID channelId, m::Conf::Data&);
+       ~gdSampleEditor();
+
+       void rebuild() override;
+       void refresh() override;
+
+       geChoice* grid;
+       geCheck*  snap;
+       geBox*    sep1;
+       geButton* zoomIn;
+       geButton* zoomOut;
+
+       geWaveTools* waveTools;
+
+       geVolumeTool* volumeTool;
+       gePanTool*    panTool;
+
+       gePitchTool* pitchTool;
+
+       geRangeTool* rangeTool;
+       geShiftTool* shiftTool;
+       geButton*    reload;
+
+       geStatusButton* play;
+       geButton*       rewind;
+       geCheck*        loop;
+       geBox*          info;
+
+private:
+       gePack*  createUpperBar();
+       gePack*  createBottomBar(int x, int y, int h);
+       geGroup* createPreviewBox(int x, int y, int h);
+       gePack*  createOpTools(int x, int y);
+
+       static void cb_reload(Fl_Widget* /*w*/, void* p);
+       static void cb_zoomIn(Fl_Widget* /*w*/, void* p);
+       static void cb_zoomOut(Fl_Widget* /*w*/, void* p);
+       static void cb_enableSnap(Fl_Widget* /*w*/, void* p);
+       static void cb_togglePreview(Fl_Widget* /*w*/, void* p);
+       static void cb_rewindPreview(Fl_Widget* /*w*/, void* p);
+       void        cb_reload();
+       void        cb_zoomIn();
+       void        cb_zoomOut();
+       void        cb_enableSnap();
+       void        cb_togglePreview();
+       void        cb_rewindPreview();
+
+       void updateInfo();
+
+       ID m_channelId;
+
+       c::sampleEditor::Data m_data;
+       m::Conf::Data&        m_conf;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dialogs/warnings.cpp b/src/gui/dialogs/warnings.cpp
new file mode 100644 (file)
index 0000000..48964fe
--- /dev/null
@@ -0,0 +1,92 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "warnings.h"
+#include "core/const.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "utils/gui.h"
+#include "window.h"
+#include <FL/Fl.H>
+#include <FL/Fl_Window.H>
+
+namespace giada::v
+{
+namespace
+{
+bool confirmRet_ = false;
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+void gdAlert(const char* msg)
+{
+       gdWindow win(u::gui::getCenterWinBounds(300, 90), "Alert");
+       win.set_modal();
+       win.begin();
+       geBox*    box = new geBox(10, 10, 280, 40, msg);
+       geButton* b   = new geButton(210, 60, 80, 20, "Close");
+       win.end();
+       box->labelsize(G_GUI_FONT_SIZE_BASE);
+
+       b->shortcut(FL_Enter);
+       b->onClick = [&win]() { win.hide(); };
+
+       u::gui::setFavicon(&win);
+       win.show();
+
+       while (win.shown())
+               Fl::wait();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int gdConfirmWin(const char* title, const char* msg)
+{
+       gdWindow win(u::gui::getCenterWinBounds(300, 90), title);
+       win.set_modal();
+       win.begin();
+       new geBox(10, 10, 280, 40, msg);
+       geButton* ok = new geButton(212, 62, 80, 20, "Ok");
+       geButton* ko = new geButton(124, 62, 80, 20, "Cancel");
+       win.end();
+
+       ok->shortcut(FL_Enter);
+       ok->onClick = [&win]() { confirmRet_ = true; win.hide(); };
+
+       ko->onClick = [&win]() { confirmRet_ = false; win.hide(); };
+
+       u::gui::setFavicon(&win);
+       win.show();
+
+       while (win.shown())
+               Fl::wait();
+
+       return confirmRet_;
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dialogs/warnings.h b/src/gui/dialogs/warnings.h
new file mode 100644 (file)
index 0000000..22b4a42
--- /dev/null
@@ -0,0 +1,36 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_WARNINGS_H
+#define GD_WARNINGS_H
+
+namespace giada::v
+{
+void gdAlert(const char* c);
+int  gdConfirmWin(const char* title, const char* msg);
+} // namespace v
+
+#endif
diff --git a/src/gui/dialogs/window.cpp b/src/gui/dialogs/window.cpp
new file mode 100644 (file)
index 0000000..9b0196f
--- /dev/null
@@ -0,0 +1,183 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "window.h"
+#include "utils/log.h"
+#include <FL/Fl.H>
+
+namespace giada::v
+{
+gdWindow::gdWindow(int x, int y, int w, int h, const char* title, int id)
+: Fl_Double_Window(x, y, w, h, title)
+, id(id)
+, parent(nullptr)
+{
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdWindow::gdWindow(int w, int h, const char* title, int id)
+: Fl_Double_Window(w, h, title)
+, id(id)
+, parent(nullptr)
+{
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdWindow::gdWindow(geompp::Rect<int> r, const char* title, int id)
+: gdWindow(r.x, r.y, r.w, r.h, title, id)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdWindow::~gdWindow()
+{
+       /* delete all subwindows in order to empty the stack */
+
+       for (unsigned j = 0; j < subWindows.size(); j++)
+               delete subWindows.at(j);
+       subWindows.clear();
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* this is the default callback of each window, fired when the user closes
+ * the window with the 'x'. Watch out: is the parent that calls delSubWIndow */
+
+void gdWindow::cb_closeChild(Fl_Widget* w, void* /*p*/)
+{
+       /* Disable default FLTK behavior where 'escape' closes the window. */
+       if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
+               return;
+
+       gdWindow* child = (gdWindow*)w;
+       if (child->getParent() != nullptr)
+               (child->getParent())->delSubWindow(child);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdWindow::addSubWindow(gdWindow* w)
+{
+       w->setParent(this);
+       w->callback(cb_closeChild); // you can pass params: w->callback(cb_closeChild, (void*)params)
+       subWindows.push_back(w);
+       //debug();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdWindow::delSubWindow(gdWindow* w)
+{
+       for (unsigned j = 0; j < subWindows.size(); j++)
+               if (w->getId() == subWindows.at(j)->getId())
+               {
+                       delete subWindows.at(j);
+                       subWindows.erase(subWindows.begin() + j);
+                       return;
+               }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdWindow::delSubWindow(int wid)
+{
+       for (unsigned j = 0; j < subWindows.size(); j++)
+               if (subWindows.at(j)->getId() == wid)
+               {
+                       delete subWindows.at(j);
+                       subWindows.erase(subWindows.begin() + j);
+                       return;
+               }
+}
+
+/* -------------------------------------------------------------------------- */
+
+int gdWindow::getId() const
+{
+       return id;
+}
+
+void gdWindow::setId(int wid)
+{
+       id = wid;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gdWindow::debug() const
+{
+       /* TODO - use G_DEBUG
+       u::log::print("---- window stack (id=%d): ----\n", getId());
+       for (unsigned i=0; i<subWindows.size(); i++)
+               u::log::print("[gdWindow] %p (id=%d)\n", (void*)subWindows.at(i), subWindows.at(i)->getId());
+       u::log::print("----\n");
+       */
+}
+
+/* -------------------------------------------------------------------------- */
+
+geompp::Rect<int> gdWindow::getContentBounds() const
+{
+       return {0, 0, w(), h()};
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdWindow* gdWindow::getParent()
+{
+       return parent;
+}
+
+void gdWindow::setParent(gdWindow* w)
+{
+       parent = w;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool gdWindow::hasWindow(int wid) const
+{
+       for (unsigned j = 0; j < subWindows.size(); j++)
+               if (wid == subWindows.at(j)->getId())
+                       return true;
+       return false;
+}
+
+/* -------------------------------------------------------------------------- */
+
+gdWindow* gdWindow::getChild(int wid)
+{
+       for (unsigned j = 0; j < subWindows.size(); j++)
+               if (wid == subWindows.at(j)->getId())
+                       return subWindows.at(j);
+       return nullptr;
+}
+} // namespace giada::v
diff --git a/src/gui/dialogs/window.h b/src/gui/dialogs/window.h
new file mode 100644 (file)
index 0000000..aac3f19
--- /dev/null
@@ -0,0 +1,79 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GD_WINDOW_H
+#define GD_WINDOW_H
+
+#include "deps/geompp/src/rect.hpp"
+#include <FL/Fl_Double_Window.H>
+#include <vector>
+
+namespace giada::v
+{
+class gdWindow : public Fl_Double_Window
+{
+public:
+       gdWindow(int x, int y, int w, int h, const char* title = 0, int id = 0);
+       gdWindow(int w, int h, const char* title = 0, int id = 0);
+       gdWindow(geompp::Rect<int>, const char* title = 0, int id = 0);
+       ~gdWindow();
+
+       static void cb_closeChild(Fl_Widget* /*w*/, void* p);
+
+       /* rebuild, refresh
+       Rebuild() is called by the View Updater when something structural changes
+       (e.g. a new channel added). Refresh() is called periodically by the View 
+       Updater during the refresh loop. */
+
+       virtual void rebuild(){};
+       virtual void refresh(){};
+
+       /* hasWindow
+       True if the window with id 'id' exists in the stack. */
+
+       bool hasWindow(int id) const;
+
+       int  getId() const;
+       void debug() const;
+
+       geompp::Rect<int> getContentBounds() const;
+
+       void      addSubWindow(gdWindow* w);
+       void      delSubWindow(gdWindow* w);
+       void      delSubWindow(int id);
+       void      setId(int id);
+       void      setParent(gdWindow* w);
+       gdWindow* getParent();
+       gdWindow* getChild(int id);
+
+protected:
+       std::vector<gdWindow*> subWindows;
+       int                    id;
+       gdWindow*              parent;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/dispatcher.cpp b/src/gui/dispatcher.cpp
new file mode 100644 (file)
index 0000000..423f75f
--- /dev/null
@@ -0,0 +1,128 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "dispatcher.h"
+#include "core/init.h"
+#include "glue/events.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/ui.h"
+#include <FL/Fl.H>
+#include <cassert>
+
+extern giada::v::Ui g_ui;
+
+namespace giada::v
+{
+Dispatcher::Dispatcher(const m::Conf::KeyBindings& k)
+: m_keyBindings(k)
+, m_keyPressed(-1)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Dispatcher::perform(ID channelId, int event) const
+{
+       if (event == FL_KEYDOWN)
+       {
+               if (Fl::event_ctrl())
+                       c::events::toggleMuteChannel(channelId, Thread::MAIN);
+               else if (Fl::event_shift())
+                       c::events::killChannel(channelId, Thread::MAIN);
+               else
+                       c::events::pressChannel(channelId, G_MAX_VELOCITY, Thread::MAIN);
+       }
+       else if (event == FL_KEYUP)
+               c::events::releaseChannel(channelId, Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+/* Walk channels array, trying to match button's bound key with the event. If 
+found, trigger the key-press/key-release function. */
+
+void Dispatcher::dispatchChannels(int event) const
+{
+       g_ui.mainWindow->keyboard->forEachChannel([=](geChannel& c) {
+               if (c.handleKey(event))
+                       perform(c.getData().id, event);
+       });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Dispatcher::dispatchKey(int event)
+{
+       assert(onEventOccured != nullptr);
+
+       /* These events come from the keyboard, not from a direct interaction on the 
+       UI with the mouse/touch. */
+
+       if (event == FL_KEYDOWN)
+       {
+               if (m_keyPressed == Fl::event_key()) // Avoid key retrig
+                       return;
+
+               m_keyPressed = Fl::event_key();
+
+               if (m_keyPressed == m_keyBindings.at(m::Conf::KEY_BIND_PLAY))
+                       c::events::toggleSequencer(Thread::MAIN);
+               else if (m_keyPressed == m_keyBindings.at(m::Conf::KEY_BIND_REWIND))
+                       c::events::rewindSequencer(Thread::MAIN);
+               else if (m_keyPressed == m_keyBindings.at(m::Conf::KEY_BIND_RECORD_ACTIONS))
+                       c::events::toggleActionRecording();
+               else if (m_keyPressed == m_keyBindings.at(m::Conf::KEY_BIND_RECORD_INPUT))
+                       c::events::toggleInputRecording();
+               else if (m_keyPressed == m_keyBindings.at(m::Conf::KEY_BIND_EXIT))
+               {
+                       c::events::stopActionRecording();
+                       c::events::stopInputRecording();
+               }
+               else
+               {
+                       onEventOccured();
+                       dispatchChannels(event);
+               }
+       }
+       else if (event == FL_KEYUP)
+       {
+               m_keyPressed = -1;
+               dispatchChannels(event);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Dispatcher::dispatchTouch(const geChannel& gch, bool status)
+{
+       assert(onEventOccured != nullptr);
+
+       onEventOccured();
+       perform(gch.getData().id, status ? FL_KEYDOWN : FL_KEYUP);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/dispatcher.h b/src/gui/dispatcher.h
new file mode 100644 (file)
index 0000000..81387c6
--- /dev/null
@@ -0,0 +1,72 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_V_DISPATCHER_H
+#define G_V_DISPATCHER_H
+
+#include "core/conf.h"
+#include "core/types.h"
+#include <functional>
+
+namespace giada::v
+{
+class geChannel;
+class Dispatcher final
+{
+public:
+       Dispatcher(const m::Conf::KeyBindings& m_keyBindings);
+
+       /* dispatchKey
+    Processes a key pressed on the physical keyboard. */
+
+       void dispatchKey(int event);
+
+       /* dispatchTouch
+    Processes a mouse click/touch event. */
+
+       void dispatchTouch(const geChannel& gch, bool status);
+
+       /* onEventOccured
+    Callback fired when a key has been pressed or a mouse button clicked. */
+
+       std::function<void()> onEventOccured;
+
+private:
+       void perform(ID channelId, int event) const;
+
+       /* dispatchChannels
+    Walks channels array, trying to match button's bound key with the event. If 
+    found, trigger the key-press/key-release function. */
+
+       void dispatchChannels(int event) const;
+
+       const m::Conf::KeyBindings& m_keyBindings;
+
+       int m_keyPressed;
+};
+} // namespace giada::v
+
+#endif
\ No newline at end of file
diff --git a/src/gui/drawing.cpp b/src/gui/drawing.cpp
new file mode 100644 (file)
index 0000000..15d192f
--- /dev/null
@@ -0,0 +1,63 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "drawing.h"
+#include "utils/gui.h"
+#include <FL/Fl.H>
+#include <cassert>
+
+namespace giada::v
+{
+void drawRectf(geompp::Rect<int> r, Fl_Color c)
+{
+       fl_rectf(r.x, r.y, r.w, r.h, c);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void drawRect(geompp::Rect<int> r, Fl_Color c)
+{
+       fl_rect(r.x, r.y, r.w, r.h, c);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void drawLine(geompp::Line<int> l, Fl_Color c)
+{
+       fl_color(c);
+       fl_line(l.x1, l.y1, l.x2, l.y2);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void drawText(const std::string& s, geompp::Rect<int> b, Fl_Color c, int alignment)
+{
+       assert(!s.empty());
+
+       fl_color(c);
+       fl_draw(u::gui::truncate(s, b.w - 16).c_str(), b.x, b.y, b.w, b.h, alignment);
+}
+} // namespace giada::v
diff --git a/src/gui/drawing.h b/src/gui/drawing.h
new file mode 100644 (file)
index 0000000..2882a0f
--- /dev/null
@@ -0,0 +1,42 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_V_DRAWING_H
+#define G_V_DRAWING_H
+
+#include "deps/geompp/src/rect.hpp"
+#include <FL/fl_draw.H>
+#include <string>
+
+namespace giada::v
+{
+void drawRectf(geompp::Rect<int>, Fl_Color);
+void drawRect(geompp::Rect<int>, Fl_Color);
+void drawLine(geompp::Line<int>, Fl_Color);
+void drawText(const std::string&, geompp::Rect<int>, Fl_Color c, int alignment = FL_ALIGN_CENTER);
+} // namespace giada::v
+
+#endif
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/baseAction.cpp b/src/gui/elems/actionEditor/baseAction.cpp
new file mode 100644 (file)
index 0000000..989f8f0
--- /dev/null
@@ -0,0 +1,129 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "baseAction.h"
+#include <FL/Fl.H>
+#include <FL/fl_draw.H>
+
+namespace giada
+{
+namespace v
+{
+geBaseAction::geBaseAction(Pixel X, Pixel Y, Pixel W, Pixel H, bool resizable,
+    m::Action a1, m::Action a2)
+: Fl_Box(X, Y, W, H)
+, onRightEdge(false)
+, onLeftEdge(false)
+, hovered(false)
+, altered(false)
+, pick(0)
+, a1(a1)
+, a2(a2)
+, m_resizable(resizable)
+{
+       if (w() < MIN_WIDTH)
+               size(MIN_WIDTH, h());
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geBaseAction::handle(int e)
+{
+       switch (e)
+       {
+       case FL_ENTER:
+       {
+               hovered = true;
+               redraw();
+               return 1;
+       }
+       case FL_LEAVE:
+       {
+               fl_cursor(FL_CURSOR_DEFAULT, FL_WHITE, FL_BLACK);
+               hovered = false;
+               redraw();
+               return 1;
+       }
+       case FL_MOVE:
+       {
+               if (m_resizable)
+               {
+                       onLeftEdge  = false;
+                       onRightEdge = false;
+                       if (Fl::event_x() >= x() && Fl::event_x() < x() + HANDLE_WIDTH)
+                       {
+                               onLeftEdge = true;
+                               fl_cursor(FL_CURSOR_WE, FL_WHITE, FL_BLACK);
+                       }
+                       else if (Fl::event_x() >= x() + w() - HANDLE_WIDTH &&
+                                Fl::event_x() <= x() + w())
+                       {
+                               onRightEdge = true;
+                               fl_cursor(FL_CURSOR_WE, FL_WHITE, FL_BLACK);
+                       }
+                       else
+                               fl_cursor(FL_CURSOR_DEFAULT, FL_WHITE, FL_BLACK);
+               }
+               return 1;
+       }
+       default:
+               return Fl_Widget::handle(e);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBaseAction::setLeftEdge(Pixel p)
+{
+       resize(p, y(), x() - p + w(), h());
+       if (w() < MIN_WIDTH)
+               size(MIN_WIDTH, h());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBaseAction::setRightEdge(Pixel p)
+{
+       size(p, h());
+       if (w() < MIN_WIDTH)
+               size(MIN_WIDTH, h());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBaseAction::setPosition(Pixel p)
+{
+       position(p, y());
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geBaseAction::isOnEdges() const
+{
+       return onLeftEdge || onRightEdge;
+}
+} // namespace v
+} // namespace giada
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/baseAction.h b/src/gui/elems/actionEditor/baseAction.h
new file mode 100644 (file)
index 0000000..35c3738
--- /dev/null
@@ -0,0 +1,75 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_BASE_ACTION_H
+#define GE_BASE_ACTION_H
+
+#include "core/types.h"
+#include "src/core/actions/actions.h"
+#include <FL/Fl_Box.H>
+
+namespace giada::m
+{
+struct Action;
+}
+namespace giada::v
+{
+class geBaseAction : public Fl_Box
+{
+public:
+       static const Pixel MIN_WIDTH    = 12;
+       static const Pixel HANDLE_WIDTH = 6;
+
+       geBaseAction(Pixel x, Pixel y, Pixel w, Pixel h, bool resizable,
+           m::Action a1, m::Action a2);
+
+       int handle(int e) override;
+
+       bool isOnEdges() const;
+
+       /* setLeftEdge/setRightEdge
+       Set new left/right edges position, relative range. */
+
+       void setLeftEdge(Pixel p);
+       void setRightEdge(Pixel p);
+
+       void setPosition(Pixel p);
+
+       bool  onRightEdge;
+       bool  onLeftEdge;
+       bool  hovered;
+       bool  altered;
+       Pixel pick;
+
+       m::Action a1;
+       m::Action a2;
+
+protected:
+       bool m_resizable;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/baseActionEditor.cpp b/src/gui/elems/actionEditor/baseActionEditor.cpp
new file mode 100644 (file)
index 0000000..2ae40ac
--- /dev/null
@@ -0,0 +1,182 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/dialogs/actionEditor/baseActionEditor.h"
+#include "baseAction.h"
+#include "baseActionEditor.h"
+#include "core/const.h"
+#include "core/sequencer.h"
+#include "gridTool.h"
+#include <FL/Fl.H>
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geBaseActionEditor::geBaseActionEditor(Pixel x, Pixel y, Pixel w, Pixel h,
+    gdBaseActionEditor* base)
+: Fl_Group(x, y, w, h)
+, m_data(nullptr)
+, m_base(base)
+, m_action(nullptr)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+geBaseAction* geBaseActionEditor::getActionAtCursor() const
+{
+       for (int i = 0; i < children(); i++)
+       {
+               geBaseAction* a = static_cast<geBaseAction*>(child(i));
+               if (a->hovered)
+                       return a;
+       }
+       return nullptr;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBaseActionEditor::baseDraw(bool clear) const
+{
+       /* Clear the screen. */
+
+       if (clear)
+               fl_rectf(x(), y(), w(), h(), G_COLOR_GREY_1);
+
+       /* Draw the outer container. */
+
+       fl_color(G_COLOR_GREY_4);
+       fl_rect(x(), y(), w(), h());
+
+       /* Draw grid, beats and bars. A grid set to 1 has a cell size == beat, so
+       painting it is useless. */
+
+       if (m_base->gridTool.getValue() > 1)
+       {
+               fl_color(G_COLOR_GREY_3);
+               drawVerticals(m_base->gridTool.getCellSize(m_data->framesInBeat));
+       }
+
+       fl_color(G_COLOR_GREY_4);
+       drawVerticals(m_data->framesInBeat);
+
+       fl_color(G_COLOR_LIGHT_1);
+       drawVerticals(m_data->framesInBar);
+
+       /* Cover unused area. Avoid drawing cover if width == 0 (i.e. beats are 32). */
+
+       Pixel coverWidth = m_base->fullWidth - m_base->loopWidth;
+       if (coverWidth != 0)
+               fl_rectf(m_base->loopWidth + x(), y() + 1, coverWidth, h() - 2, G_COLOR_GREY_4);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBaseActionEditor::drawVerticals(int steps) const
+{
+       /* Start drawing from steps, not from 0. The zero-th element is always 
+       graphically useless. */
+       for (Frame i = steps; i < m_data->framesInLoop; i += steps)
+       {
+               Pixel p = m_base->frameToPixel(i) + x();
+               fl_line(p, y() + 1, p, y() + h() - 2);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geBaseActionEditor::handle(int e)
+{
+       switch (e)
+       {
+       case FL_PUSH:
+               return push();
+       case FL_DRAG:
+               return drag();
+       case FL_RELEASE:
+               fl_cursor(FL_CURSOR_DEFAULT, FL_WHITE, FL_BLACK); // Make sure cursor returns normal
+               return release();
+       default:
+               return Fl_Group::handle(e);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geBaseActionEditor::push()
+{
+       m_action = getActionAtCursor();
+
+       if (Fl::event_button1())
+       { // Left button
+               if (m_action == nullptr)
+               {                                          // No action under cursor: add a new one
+                       if (Fl::event_x() < m_base->loopWidth) // Avoid click on grey area
+                               onAddAction();
+               }
+               else // Prepare for dragging
+                       m_action->pick = Fl::event_x() - m_action->x();
+       }
+       else if (Fl::event_button3())
+       { // Right button
+               if (m_action != nullptr)
+               {
+                       onDeleteAction();
+                       m_action = nullptr;
+               }
+       }
+       return 1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geBaseActionEditor::drag()
+{
+       if (m_action == nullptr)
+               return 0;
+       if (m_action->isOnEdges())
+               onResizeAction();
+       else
+               onMoveAction();
+       m_action->altered = true;
+       redraw();
+       return 1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geBaseActionEditor::release()
+{
+       int ret = 0;
+       if (m_action != nullptr && m_action->altered)
+       {
+               onRefreshAction();
+               ret = 1;
+       }
+       m_action = nullptr;
+       return ret;
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/baseActionEditor.h b/src/gui/elems/actionEditor/baseActionEditor.h
new file mode 100644 (file)
index 0000000..dd47439
--- /dev/null
@@ -0,0 +1,100 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_BASE_ACTION_EDITOR_H
+#define GE_BASE_ACTION_EDITOR_H
+
+#include "core/types.h"
+#include <FL/Fl_Group.H>
+
+namespace giada::c::actionEditor
+{
+struct Data;
+}
+
+namespace giada::v
+{
+class gdBaseActionEditor;
+class geBaseAction;
+class geBaseActionEditor : public Fl_Group
+{
+public:
+       /* updateActions
+       Rebuild the actions widgets from scratch. */
+
+       virtual void rebuild(c::actionEditor::Data& d) = 0;
+
+       /* handle
+       Override base FL_Group events. */
+
+       int handle(int e) override;
+
+       /* getActionAtCursor
+       Returns the action under the mouse. nullptr if nothing found. Why not using
+       Fl::belowmouse? It would require a boring dynamic_cast. */
+
+       geBaseAction* getActionAtCursor() const;
+
+protected:
+       geBaseActionEditor(Pixel x, Pixel y, Pixel w, Pixel h, gdBaseActionEditor*);
+
+       c::actionEditor::Data* m_data;
+
+       /* m_base
+       Pointer to parent class. */
+
+       gdBaseActionEditor* m_base;
+
+       /* m_action
+       Selected action. Used while dragging. */
+
+       geBaseAction* m_action;
+
+       /* baseDraw
+       Draws basic things like borders and grids. Optional background clear. */
+
+       void baseDraw(bool clear = true) const;
+
+       virtual void onAddAction()     = 0;
+       virtual void onDeleteAction()  = 0;
+       virtual void onMoveAction()    = 0;
+       virtual void onResizeAction()  = 0;
+       virtual void onRefreshAction() = 0;
+
+private:
+       /* drawVerticals
+       Draws generic vertical lines (beats, bars, grid lines...). */
+
+       void drawVerticals(int steps) const;
+
+       int push();
+       int drag();
+       int release();
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/envelopeEditor.cpp b/src/gui/elems/actionEditor/envelopeEditor.cpp
new file mode 100644 (file)
index 0000000..e6ad8f9
--- /dev/null
@@ -0,0 +1,209 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "envelopeEditor.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "envelopePoint.h"
+#include "glue/actionEditor.h"
+#include "glue/channel.h"
+#include "gui/dialogs/actionEditor/baseActionEditor.h"
+#include "src/core/actions/action.h"
+#include "src/core/actions/actions.h"
+#include "utils/log.h"
+#include "utils/math.h"
+#include <FL/Fl.H>
+#include <FL/fl_draw.H>
+#include <cassert>
+
+namespace giada::v
+{
+geEnvelopeEditor::geEnvelopeEditor(Pixel x, Pixel y, const char* l, gdBaseActionEditor* b)
+: geBaseActionEditor(x, y, 200, 40, b)
+{
+       copy_label(l);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geEnvelopeEditor::draw()
+{
+       baseDraw();
+
+       /* Print label. */
+
+       fl_color(G_COLOR_GREY_4);
+       fl_font(FL_HELVETICA, G_GUI_FONT_SIZE_BASE);
+       fl_draw(label(), x() + 4, y(), w(), h(), (Fl_Align)(FL_ALIGN_LEFT));
+
+       if (children() == 0)
+               return;
+
+       Pixel side = geEnvelopePoint::SIDE / 2;
+
+       Pixel x1 = child(0)->x() + side;
+       Pixel y1 = child(0)->y() + side;
+       Pixel x2 = 0;
+       Pixel y2 = 0;
+
+       /* For each point: 
+               - paint the connecting line with the next one;
+               - reposition it on the y axis, only if there's no point selected (dragged
+           around). */
+
+       for (int i = 0; i < children(); i++)
+       {
+               geEnvelopePoint* p = static_cast<geEnvelopePoint*>(child(i));
+               if (m_action == nullptr)
+                       p->position(p->x(), valueToY(p->a1.event.getVelocity()));
+               if (i > 0)
+               {
+                       x2 = p->x() + side;
+                       y2 = p->y() + side;
+                       fl_line(x1, y1, x2, y2);
+                       x1 = x2;
+                       y1 = y2;
+               }
+       }
+
+       draw_children();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geEnvelopeEditor::rebuild(c::actionEditor::Data& d)
+{
+       m_data = &d;
+
+       /* Remove all existing actions and set a new width, according to the current
+       zoom level. */
+
+       clear();
+       size(m_base->fullWidth, h());
+
+       for (const m::Action& a : m_data->actions)
+       {
+               if (a.event.getStatus() != m::MidiEvent::ENVELOPE)
+                       continue;
+               add(new geEnvelopePoint(frameToX(a.frame), valueToY(a.event.getVelocity()), a));
+       }
+
+       resizable(nullptr);
+
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geEnvelopeEditor::isFirstPoint() const
+{
+       return find(m_action) == 0;
+}
+
+bool geEnvelopeEditor::isLastPoint() const
+{
+       return find(m_action) == children() - 1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Pixel geEnvelopeEditor::frameToX(Frame frame) const
+{
+       return x() + m_base->frameToPixel(frame) - (geEnvelopePoint::SIDE / 2);
+}
+
+Pixel geEnvelopeEditor::valueToY(int value) const
+{
+       return u::math::map<int, Pixel>(value, 0, G_MAX_VELOCITY, y() + (h() - geEnvelopePoint::SIDE), y());
+}
+
+int geEnvelopeEditor::yToValue(Pixel pixel, Pixel offset) const
+{
+       return u::math::map<Pixel, int>(pixel, h() - offset, 0, 0, G_MAX_VELOCITY);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geEnvelopeEditor::onAddAction()
+{
+       Frame f = m_base->pixelToFrame(Fl::event_x() - x(), m_data->framesInBeat);
+       int   v = yToValue(Fl::event_y() - y());
+
+       c::actionEditor::recordEnvelopeAction(m_data->channelId, f, v);
+
+       m_base->rebuild(); // TODO - USELESS
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geEnvelopeEditor::onDeleteAction()
+{
+       c::actionEditor::deleteEnvelopeAction(m_data->channelId, m_action->a1);
+
+       m_base->rebuild(); // TODO - USELESS
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geEnvelopeEditor::onMoveAction()
+{
+       Pixel side = geEnvelopePoint::SIDE / 2;
+       Pixel ex   = Fl::event_x() - side;
+       Pixel ey   = Fl::event_y() - side;
+
+       Pixel x1 = x() - side;
+       Pixel x2 = m_base->loopWidth + x() - side;
+       Pixel y1 = y();
+       Pixel y2 = y() + h() - geEnvelopePoint::SIDE;
+
+       /* x-axis constraints. */
+       if (isFirstPoint() || ex < x1)
+               ex = x1;
+       else if (isLastPoint() || ex > x2)
+               ex = x2;
+
+       /* y-axis constraints. */
+       if (ey < y1)
+               ey = y1;
+       else if (ey > y2)
+               ey = y2;
+
+       m_action->position(ex, ey);
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geEnvelopeEditor::onRefreshAction()
+{
+       const Frame f  = (m_action->x() - x()) + geEnvelopePoint::SIDE / 2;
+       const Frame fq = m_base->pixelToFrame(f, m_data->framesInBeat);
+       const float v  = yToValue(m_action->y() - y(), geEnvelopePoint::SIDE);
+       c::actionEditor::updateEnvelopeAction(m_data->channelId, m_action->a1, fq, v);
+
+       m_base->rebuild();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/envelopeEditor.h b/src/gui/elems/actionEditor/envelopeEditor.h
new file mode 100644 (file)
index 0000000..494492d
--- /dev/null
@@ -0,0 +1,58 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_ENVELOPE_EDITOR_H
+#define GE_ENVELOPE_EDITOR_H
+
+#include "baseActionEditor.h"
+
+namespace giada::v
+{
+class geEnvelopeEditor : public geBaseActionEditor
+{
+public:
+       geEnvelopeEditor(Pixel x, Pixel y, const char* l, gdBaseActionEditor*);
+       void draw() override;
+
+       void rebuild(c::actionEditor::Data& d) override;
+
+private:
+       void onAddAction() override;
+       void onDeleteAction() override;
+       void onMoveAction() override;
+       void onResizeAction() override{}; // Nothing to do here
+       void onRefreshAction() override;
+
+       Pixel frameToX(Frame frame) const;
+       Pixel valueToY(int value) const;
+       int   yToValue(Pixel pixel, Pixel offset = 0) const;
+
+       bool isFirstPoint() const;
+       bool isLastPoint() const;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/envelopePoint.cpp b/src/gui/elems/actionEditor/envelopePoint.cpp
new file mode 100644 (file)
index 0000000..d39998d
--- /dev/null
@@ -0,0 +1,44 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "envelopePoint.h"
+#include "core/const.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geEnvelopePoint::geEnvelopePoint(Pixel X, Pixel Y, m::Action a)
+: geBaseAction(X, Y, SIDE, SIDE, /*resizable=*/false, a, {})
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geEnvelopePoint::draw()
+{
+       fl_rectf(x(), y(), w(), h(), hovered ? G_COLOR_LIGHT_2 : G_COLOR_LIGHT_1);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/envelopePoint.h b/src/gui/elems/actionEditor/envelopePoint.h
new file mode 100644 (file)
index 0000000..ffc51b0
--- /dev/null
@@ -0,0 +1,46 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_ENVELOPE_POINT_H
+#define GE_ENVELOPE_POINT_H
+
+#include "baseAction.h"
+#include "src/core/actions/actions.h"
+
+namespace giada::v
+{
+class geEnvelopePoint : public geBaseAction
+{
+public:
+       static const Pixel SIDE = 12;
+
+       geEnvelopePoint(Pixel x, Pixel y, m::Action a);
+
+       void draw() override;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/gridTool.cpp b/src/gui/elems/actionEditor/gridTool.cpp
new file mode 100644 (file)
index 0000000..97ce87f
--- /dev/null
@@ -0,0 +1,121 @@
+/* -----------------------------------------------------------------------------
+*
+* Giada - Your Hardcore Loopmachine
+*
+* ------------------------------------------------------------------------------
+*
+* Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+*
+* This file is part of Giada - Your Hardcore Loopmachine.
+*
+* Giada - Your Hardcore Loopmachine 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.
+*
+* Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+* <http://www.gnu.org/licenses/>.
+*
+* --------------------------------------------------------------------------- */
+
+#include "gui/elems/actionEditor/gridTool.h"
+#include "core/conf.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/choice.h"
+#include "utils/math.h"
+#include <FL/Fl_Double_Window.H>
+
+namespace giada::v
+{
+geGridTool::geGridTool(Pixel x, Pixel y, m::Conf::Data& c)
+: Fl_Group(x, y, 80, 20)
+, m_conf(c)
+{
+       gridType = new geChoice(x, y, 40, 20);
+       gridType->addItem("1");
+       gridType->addItem("2");
+       gridType->addItem("3");
+       gridType->addItem("4");
+       gridType->addItem("6");
+       gridType->addItem("8");
+       gridType->addItem("16");
+       gridType->addItem("32");
+       gridType->showItem(0);
+       gridType->onChange = [this](ID) {
+               window()->redraw();
+       };
+
+       active = new geCheck(gridType->x() + gridType->w() + 4, y, 20, 20);
+
+       gridType->showItem(m_conf.actionEditorGridVal);
+       active->value(m_conf.actionEditorGridOn);
+
+       end();
+
+       gridType->copy_tooltip("Grid resolution");
+       active->copy_tooltip("Snap to grid");
+}
+
+/* -------------------------------------------------------------------------- */
+
+geGridTool::~geGridTool()
+{
+       m_conf.actionEditorGridVal = gridType->getSelectedId();
+       m_conf.actionEditorGridOn  = active->value();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geGridTool::isOn() const
+{
+       return active->value();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geGridTool::getValue() const
+{
+       switch (gridType->getSelectedId())
+       {
+       case 0:
+               return 1;
+       case 1:
+               return 2;
+       case 2:
+               return 3;
+       case 3:
+               return 4;
+       case 4:
+               return 6;
+       case 5:
+               return 8;
+       case 6:
+               return 16;
+       case 7:
+               return 32;
+       }
+       return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame geGridTool::getSnapFrame(Frame v, Frame framesInBeat) const
+{
+       if (!isOn())
+               return v;
+       return u::math::quantize(v, getCellSize(framesInBeat));
+}
+
+/* -------------------------------------------------------------------------- */
+
+Frame geGridTool::getCellSize(Frame framesInBeat) const
+{
+       return framesInBeat / getValue();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/gridTool.h b/src/gui/elems/actionEditor/gridTool.h
new file mode 100644 (file)
index 0000000..ffa9660
--- /dev/null
@@ -0,0 +1,63 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_GRID_TOOL_H
+#define GE_GRID_TOOL_H
+
+#include "core/conf.h"
+#include "core/types.h"
+#include <FL/Fl_Group.H>
+
+class geCheck;
+
+namespace giada::v
+{
+class geChoice;
+class geGridTool : public Fl_Group
+{
+public:
+       geGridTool(Pixel x, Pixel y, m::Conf::Data&);
+       ~geGridTool();
+
+       int  getValue() const;
+       bool isOn() const;
+
+       Frame getSnapFrame(Frame f, Frame framesInBeat) const;
+
+       /* getCellSize
+       Returns the size in frames of a single cell of the grid. */
+
+       Frame getCellSize(Frame framesInBeat) const;
+
+private:
+       m::Conf::Data& m_conf;
+
+       geChoice* gridType;
+       geCheck*  active;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/pianoItem.cpp b/src/gui/elems/actionEditor/pianoItem.cpp
new file mode 100644 (file)
index 0000000..83233e0
--- /dev/null
@@ -0,0 +1,93 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "pianoItem.h"
+#include "core/const.h"
+#include "core/midiEvent.h"
+#include "src/core/actions/action.h"
+#include "utils/math.h"
+#include <FL/fl_draw.H>
+
+namespace giada
+{
+namespace v
+{
+gePianoItem::gePianoItem(Pixel X, Pixel Y, Pixel W, Pixel H, m::Action a1,
+    m::Action a2)
+: geBaseAction(X, Y, W, H, /*resizable=*/true, a1, a2)
+, m_ringLoop(a2.isValid() && a1.frame > a2.frame)
+, m_orphaned(!a2.isValid())
+{
+       m_resizable = isResizable();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool gePianoItem::isResizable() const
+{
+       return !(m_ringLoop || m_orphaned);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoItem::draw()
+{
+       Fl_Color color = hovered ? G_COLOR_LIGHT_2 : G_COLOR_LIGHT_1;
+
+       Pixel by = y() + 2;
+       Pixel bh = h() - 3;
+
+       if (m_orphaned)
+       {
+               fl_rect(x(), by, w(), bh, color);
+               fl_line(x(), by, x() + w(), by + bh);
+       }
+       else
+       {
+               Pixel vh = calcVelocityH();
+               if (m_ringLoop)
+               {
+                       fl_rect(x(), by, MIN_WIDTH, bh, color);
+                       fl_line(x() + MIN_WIDTH, by + bh / 2, x() + w(), by + bh / 2);
+                       fl_rectf(x(), by + (bh - vh), MIN_WIDTH, vh, color);
+               }
+               else
+               {
+                       fl_rect(x(), by, w(), bh, color);
+                       fl_rectf(x(), by + (bh - vh), w(), vh, color);
+               }
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+Pixel gePianoItem::calcVelocityH() const
+{
+       int v = a1.event.getVelocity();
+       return u::math::map<int, Pixel>(v, 0, G_MAX_VELOCITY, 0, h() - 3);
+}
+} // namespace v
+} // namespace giada
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/pianoItem.h b/src/gui/elems/actionEditor/pianoItem.h
new file mode 100644 (file)
index 0000000..293aba5
--- /dev/null
@@ -0,0 +1,56 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_PIANO_ITEM_H
+#define GE_PIANO_ITEM_H
+
+#include "baseAction.h"
+
+namespace giada::m
+{
+struct Action;
+}
+
+namespace giada::v
+{
+class gePianoItem : public geBaseAction
+{
+public:
+       gePianoItem(int x, int y, int w, int h, m::Action a1, m::Action a2);
+
+       void draw() override;
+
+       bool isResizable() const;
+
+private:
+       bool m_ringLoop;
+       bool m_orphaned;
+
+       Pixel calcVelocityH() const;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/pianoRoll.cpp b/src/gui/elems/actionEditor/pianoRoll.cpp
new file mode 100644 (file)
index 0000000..de357a1
--- /dev/null
@@ -0,0 +1,391 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "pianoRoll.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/midiEvent.h"
+#include "glue/actionEditor.h"
+#include "glue/channel.h"
+#include "gui/dialogs/actionEditor/baseActionEditor.h"
+#include "gui/elems/actionEditor/pianoItem.h"
+#include "src/core/actions/action.h"
+#include "utils/log.h"
+#include "utils/math.h"
+#include "utils/string.h"
+#include <FL/Fl.H>
+#include <cassert>
+
+namespace giada::v
+{
+gePianoRoll::gePianoRoll(Pixel X, Pixel Y, gdBaseActionEditor* b)
+: geBaseActionEditor(X, Y, 200, CELL_H * MAX_KEYS, b)
+, m_pick(0)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::drawSurfaceY()
+{
+       surfaceY = fl_create_offscreen(CELL_W, h());
+       fl_begin_offscreen(surfaceY);
+
+       /* Warning: only w() and h() come from this widget, x and y coordinates are 
+       absolute, since we are writing in a memory chunk. */
+
+       fl_rectf(0, 0, CELL_W, h(), G_COLOR_GREY_1);
+
+       fl_line_style(FL_DASH, 0, nullptr);
+       fl_font(FL_HELVETICA, G_GUI_FONT_SIZE_BASE);
+
+       int octave = MAX_OCTAVES;
+
+       for (int i = 1; i <= MAX_KEYS + 1; i++)
+       {
+               /* print key note label. C C# D D# E F F# G G# A A# B */
+
+               std::string note = u::string::iToString(octave);
+               switch (i % KEYS)
+               {
+               case (int)Notes::G:
+                       fl_rectf(0, i * CELL_H, CELL_W, CELL_H, G_COLOR_GREY_2);
+                       note += " G";
+                       break;
+               case (int)Notes::FS:
+                       note += " F#";
+                       break;
+               case (int)Notes::F:
+                       note += " F";
+                       break;
+               case (int)Notes::E:
+                       fl_rectf(0, i * CELL_H, CELL_W, CELL_H, G_COLOR_GREY_2);
+                       note += " E";
+                       break;
+               case (int)Notes::DS:
+                       note += " D#";
+                       break;
+               case (int)Notes::D:
+                       fl_rectf(0, i * CELL_H, CELL_W, CELL_H, G_COLOR_GREY_2);
+                       note += " D";
+                       break;
+               case (int)Notes::CS:
+                       note += " C#";
+                       break;
+               case (int)Notes::C:
+                       note += " C";
+                       octave--;
+                       break;
+               case (int)Notes::B:
+                       fl_rectf(0, i * CELL_H, CELL_W, CELL_H, G_COLOR_GREY_2);
+                       note += " B";
+                       break;
+               case (int)Notes::AS:
+                       note += " A#";
+                       break;
+               case (int)Notes::A:
+                       fl_rectf(0, i * CELL_H, CELL_W, CELL_H, G_COLOR_GREY_2);
+                       note += " A";
+                       break;
+               case (int)Notes::GS:
+                       note += " G#";
+                       break;
+               }
+
+               /* Print note name */
+
+               fl_color(G_COLOR_GREY_3);
+               fl_draw(note.c_str(), 4, ((i - 1) * CELL_H) + 1, CELL_W, CELL_H,
+                   (Fl_Align)(FL_ALIGN_LEFT | FL_ALIGN_CENTER));
+
+               /* Print horizontal line */
+
+               if (i < MAX_KEYS + 1)
+                       fl_line(0, i * CELL_H, CELL_W, +i * CELL_H);
+       }
+
+       fl_line_style(0);
+       fl_end_offscreen();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::drawSurfaceX()
+{
+       surfaceX = fl_create_offscreen(CELL_W, h());
+
+       fl_begin_offscreen(surfaceX);
+       fl_rectf(0, 0, CELL_W, h(), G_COLOR_GREY_1);
+       fl_color(G_COLOR_GREY_3);
+       fl_line_style(FL_DASH, 0, nullptr);
+
+       for (int i = 1; i <= MAX_KEYS + 1; i++)
+       {
+               switch (i % KEYS)
+               {
+               case static_cast<int>(Notes::G):
+               case static_cast<int>(Notes::E):
+               case static_cast<int>(Notes::D):
+               case static_cast<int>(Notes::B):
+               case static_cast<int>(Notes::A):
+                       fl_rectf(0, i * CELL_H, CELL_W, CELL_H, G_COLOR_GREY_2);
+                       break;
+               }
+               if (i < MAX_KEYS + 1)
+               {
+                       fl_color(G_COLOR_GREY_3);
+                       fl_line(0, i * CELL_H, CELL_W, i * CELL_H);
+               }
+       }
+
+       fl_line_style(0);
+       fl_end_offscreen();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::draw()
+{
+       fl_copy_offscreen(x(), y(), CELL_W, h(), surfaceY, 0, 0);
+
+// TODO - is this APPLE thing still useful?
+#if defined(__APPLE__)
+       for (Pixel i = 36; i < m_base->fullWidth; i += 36) /// TODO: i < m_base->loopWidth is faster
+               fl_copy_offscreen(x() + i, y(), CELL_W, h(), surfaceX, 1, 0);
+#else
+       for (Pixel i = CELL_W; i < m_base->loopWidth; i += CELL_W)
+               fl_copy_offscreen(x() + i, y(), CELL_W, h(), surfaceX, 0, 0);
+#endif
+
+       baseDraw(false);
+       draw_children();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int gePianoRoll::handle(int e)
+{
+       if (!Fl::event_button3())
+               return geBaseActionEditor::handle(e);
+
+       switch (e)
+       {
+       case FL_PUSH:
+       {
+               m_pick = Fl::event_y() - y();
+               break;
+       }
+       case FL_DRAG:
+       {
+               const int pos = Fl::event_y() - m_pick;
+               const int min = parent()->y();
+               const int max = -h() + (parent()->h() + parent()->y());
+               position(x(), std::clamp(pos, max, min));
+               break;
+       }
+       default:
+               break;
+       }
+
+       return geBaseActionEditor::handle(e);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::onAddAction()
+{
+       Frame frame = m_base->pixelToFrame(Fl::event_x() - x(), m_data->framesInBeat);
+       int   note  = yToNote(Fl::event_y() - y());
+       c::actionEditor::recordMidiAction(m_data->channelId, note, G_MAX_VELOCITY,
+           frame);
+
+       m_base->rebuild(); // Rebuild velocityEditor as well
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::onDeleteAction()
+{
+       c::actionEditor::deleteMidiAction(m_data->channelId, m_action->a1);
+
+       m_base->rebuild(); // Rebuild velocityEditor as well
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::onMoveAction()
+{
+       /* Y computation:  - (CELL_H/2) is wrong: we should need the y pick value as 
+       done with x. Let's change this when vertical piano zoom will be available. */
+
+       Pixel ex = Fl::event_x() - m_action->pick;
+       Pixel ey = snapToY(Fl::event_y() - y() - (CELL_H / 2)) + y();
+
+       Pixel x1 = x();
+       Pixel x2 = (m_base->loopWidth + x()) - m_action->w();
+       Pixel y1 = y();
+       Pixel y2 = y() + h();
+
+       if (ex < x1)
+               ex = x1;
+       else if (ex > x2)
+               ex = x2;
+       if (ey < y1)
+               ey = y1;
+       else if (ey > y2)
+               ey = y2;
+
+       m_action->position(ex, ey);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::onResizeAction()
+{
+       if (!static_cast<gePianoItem*>(m_action)->isResizable())
+               return;
+
+       Pixel ex = Fl::event_x();
+
+       Pixel x1 = x();
+       Pixel x2 = m_base->loopWidth + x();
+
+       if (ex < x1)
+               ex = x1;
+       else if (ex > x2)
+               ex = x2;
+
+       if (m_action->onRightEdge)
+               m_action->setRightEdge(ex - m_action->x());
+       else
+               m_action->setLeftEdge(ex);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::onRefreshAction()
+{
+       namespace ca = c::actionEditor;
+
+       Pixel p1 = m_action->x() - x();
+       Pixel p2 = m_action->x() + m_action->w() - x();
+
+       Frame f1 = 0;
+       Frame f2 = 0;
+
+       if (!m_action->isOnEdges())
+       {
+               f1 = m_base->pixelToFrame(p1, m_data->framesInBeat);
+               f2 = m_base->pixelToFrame(p2, m_data->framesInBeat, /*snap=*/false) - (m_base->pixelToFrame(p1, m_data->framesInBeat, /*snap=*/false) - f1);
+       }
+       else if (m_action->onLeftEdge)
+       {
+               f1 = m_base->pixelToFrame(p1, m_data->framesInBeat);
+               f2 = m_action->a2.frame;
+               if (f1 == f2) // If snapping makes an action fall onto the other
+                       f1 -= G_DEFAULT_ACTION_SIZE;
+       }
+       else if (m_action->onRightEdge)
+       {
+               f1 = m_action->a1.frame;
+               f2 = m_base->pixelToFrame(p2, m_data->framesInBeat);
+               if (f1 == f2) // If snapping makes an action fall onto the other
+                       f2 += G_DEFAULT_ACTION_SIZE;
+       }
+
+       assert(f2 != 0);
+
+       int note     = yToNote(m_action->y() - y());
+       int velocity = m_action->a1.event.getVelocity();
+
+       ca::updateMidiAction(m_data->channelId, m_action->a1, note, velocity, f1, f2);
+
+       m_base->rebuild(); // Rebuild velocityEditor as well
+}
+
+/* -------------------------------------------------------------------------- */
+
+int gePianoRoll::yToNote(Pixel p) const
+{
+       return gePianoRoll::MAX_KEYS - (p / gePianoRoll::CELL_H);
+}
+
+Pixel gePianoRoll::noteToY(int n) const
+{
+       return (MAX_KEYS * CELL_H) - (n * gePianoRoll::CELL_H);
+}
+
+Pixel gePianoRoll::snapToY(Pixel p) const
+{
+       return u::math::quantize(p, CELL_H);
+}
+
+Pixel gePianoRoll::getPianoItemW(Pixel px, const m::Action& a1, const m::Action& a2) const
+{
+       if (a2.isValid())
+       {                            // Regular
+               if (a1.frame > a2.frame) // Ring-loop
+                       return m_base->loopWidth - (px - x());
+               return m_base->frameToPixel(a2.frame - a1.frame);
+       }
+       return geBaseAction::MIN_WIDTH; // Orphaned
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePianoRoll::rebuild(c::actionEditor::Data& d)
+{
+       m_data = &d;
+
+       /* Remove all existing actions and set a new width, according to the current
+       zoom level. */
+
+       clear();
+       size(m_base->fullWidth, (MAX_KEYS + 1) * CELL_H);
+
+       for (const m::Action& a1 : m_data->actions)
+       {
+               if (a1.event.getStatus() == m::MidiEvent::NOTE_OFF)
+                       continue;
+
+               assert(a1.isValid()); // a2 might be null if orphaned
+
+               const m::Action& a2 = a1.next != nullptr ? *a1.next : m::Action{};
+
+               Pixel px = x() + m_base->frameToPixel(a1.frame);
+               Pixel py = y() + noteToY(a1.event.getNote());
+               Pixel ph = CELL_H;
+               Pixel pw = getPianoItemW(px, a1, a2);
+
+               add(new gePianoItem(px, py, pw, ph, a1, a2));
+       }
+
+       drawSurfaceY();
+       drawSurfaceX();
+
+       redraw();
+}
+} // namespace giada::v
diff --git a/src/gui/elems/actionEditor/pianoRoll.h b/src/gui/elems/actionEditor/pianoRoll.h
new file mode 100644 (file)
index 0000000..4cb3f53
--- /dev/null
@@ -0,0 +1,106 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_PIANO_ROLL_H
+#define GE_PIANO_ROLL_H
+
+#include "baseActionEditor.h"
+#include <FL/fl_draw.H>
+
+namespace giada::m
+{
+struct Action;
+}
+
+namespace giada::v
+{
+class gePianoRoll : public geBaseActionEditor
+{
+public:
+       static const int   MAX_KEYS    = 127;
+       static const int   MAX_OCTAVES = 9;
+       static const int   KEYS        = 12;
+       static const Pixel CELL_H      = 20;
+       static const Pixel CELL_W      = 40;
+
+       gePianoRoll(Pixel x, Pixel y, gdBaseActionEditor* b);
+
+       void draw() override;
+       int  handle(int e) override;
+
+       void rebuild(c::actionEditor::Data& d) override;
+
+private:
+       enum class Notes
+       {
+               G  = 1,
+               FS = 2,
+               F  = 3,
+               E  = 4,
+               DS = 5,
+               D  = 6,
+               CS = 7,
+               C  = 8,
+               B  = 9,
+               AS = 10,
+               A  = 11,
+               GS = 0
+       };
+
+       void onAddAction() override;
+       void onDeleteAction() override;
+       void onMoveAction() override;
+       void onResizeAction() override;
+       void onRefreshAction() override;
+
+       /* drawSurface*
+       Generates a complex drawing in memory first and copy it to the screen at a
+       later point in time. Fl_Offscreen surface holds the necessary data.     The first
+       call creates an offscreen surface of CELL_W pixel wide containing note values.
+       The second call creates another offscreen surface of CELL_W pixels wide
+       containing the rest of the piano roll. The latter will then be tiled during
+       the ::draw() call. */
+
+       void drawSurfaceY();
+       void drawSurfaceX();
+
+       Pixel snapToY(Pixel p) const;
+       int   yToNote(Pixel y) const;
+       Pixel noteToY(int n) const;
+       Pixel getPianoItemW(Pixel x, const m::Action& a1, const m::Action& a2) const;
+
+       Fl_Offscreen surfaceY; // vertical notes, no x-repeat
+       Fl_Offscreen surfaceX; // lines, x-repeat
+
+       /* m_pick
+       Y-coordinate of the click event when the user clicks on an empty area of the
+       piano roll. Used for right mouse button scrolling. */
+
+       Pixel m_pick;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/sampleAction.cpp b/src/gui/elems/actionEditor/sampleAction.cpp
new file mode 100644 (file)
index 0000000..0c1b2b4
--- /dev/null
@@ -0,0 +1,68 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "sampleAction.h"
+#include "core/const.h"
+#include "src/core/actions/action.h"
+#include <FL/fl_draw.H>
+
+namespace giada
+{
+namespace v
+{
+geSampleAction::geSampleAction(Pixel X, Pixel Y, Pixel W, Pixel H,
+    bool singlePress, m::Action a1, m::Action a2)
+: geBaseAction(X, Y, W, H, singlePress, a1, a2)
+, m_singlePress(singlePress)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleAction::draw()
+{
+       Fl_Color color = hovered ? G_COLOR_LIGHT_2 : G_COLOR_LIGHT_1;
+
+       if (m_singlePress)
+       {
+               fl_rectf(x(), y(), w(), h(), color);
+       }
+       else
+       {
+               if (a1.event.getStatus() == m::MidiEvent::NOTE_KILL)
+                       fl_rect(x(), y(), MIN_WIDTH, h(), color);
+               else
+               {
+                       fl_rectf(x(), y(), MIN_WIDTH, h(), color);
+                       if (a1.event.getStatus() == m::MidiEvent::NOTE_ON)
+                               fl_rectf(x() + 3, y() + h() - 11, w() - 6, 8, G_COLOR_GREY_4);
+                       else if (a1.event.getStatus() == m::MidiEvent::NOTE_OFF)
+                               fl_rectf(x() + 3, y() + 3, w() - 6, 8, G_COLOR_GREY_4);
+               }
+       }
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/actionEditor/sampleAction.h b/src/gui/elems/actionEditor/sampleAction.h
new file mode 100644 (file)
index 0000000..13a2736
--- /dev/null
@@ -0,0 +1,48 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SAMPLE_ACTION_H
+#define GE_SAMPLE_ACTION_H
+
+#include "baseAction.h"
+#include "src/core/actions/actions.h"
+
+namespace giada::v
+{
+class geSampleAction : public geBaseAction
+{
+public:
+       geSampleAction(Pixel x, Pixel y, Pixel w, Pixel h, bool singlePress,
+           m::Action a1, m::Action a2);
+
+       void draw() override;
+
+private:
+       bool m_singlePress;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/sampleActionEditor.cpp b/src/gui/elems/actionEditor/sampleActionEditor.cpp
new file mode 100644 (file)
index 0000000..16c6eab
--- /dev/null
@@ -0,0 +1,209 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "sampleActionEditor.h"
+#include "core/const.h"
+#include "glue/actionEditor.h"
+#include "glue/channel.h"
+#include "gui/dialogs/actionEditor/baseActionEditor.h"
+#include "gui/dialogs/actionEditor/sampleActionEditor.h"
+#include "sampleAction.h"
+#include "src/core/actions/action.h"
+#include "src/core/actions/actions.h"
+#include "utils/log.h"
+#include <FL/Fl.H>
+#include <FL/fl_draw.H>
+#include <cassert>
+
+namespace giada::v
+{
+geSampleActionEditor::geSampleActionEditor(Pixel x, Pixel y, gdBaseActionEditor* b)
+: geBaseActionEditor(x, y, 200, 40, b)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleActionEditor::rebuild(c::actionEditor::Data& d)
+{
+       m_data = &d;
+
+       bool isSinglePressMode = m_data->sample->channelMode == SamplePlayerMode::SINGLE_PRESS;
+       bool isAnyLoopMode     = m_data->sample->isLoopMode;
+
+       /* Remove all existing actions and set a new width, according to the current
+       zoom level. */
+
+       clear();
+       size(m_base->fullWidth, h());
+
+       for (const m::Action& a1 : m_data->actions)
+       {
+               if (a1.event.getStatus() == m::MidiEvent::ENVELOPE || isNoteOffSinglePress(a1))
+                       continue;
+
+               const m::Action& a2 = a1.next != nullptr ? *a1.next : m::Action{};
+
+               Pixel px = x() + m_base->frameToPixel(a1.frame);
+               Pixel py = y() + 4;
+               Pixel pw = 0;
+               Pixel ph = h() - 8;
+               if (a2.isValid() && isSinglePressMode)
+                       pw = m_base->frameToPixel(a2.frame - a1.frame);
+
+               geSampleAction* gsa = new geSampleAction(px, py, pw, ph, isSinglePressMode, a1, a2);
+               add(gsa);
+               resizable(gsa);
+       }
+
+       /* If channel is LOOP_ANY, deactivate it: a loop mode channel cannot hold 
+       keypress/keyrelease actions. */
+
+       isAnyLoopMode ? deactivate() : activate();
+
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleActionEditor::draw()
+{
+       /* Force height to match its parent's height. This widget belongs to a 
+       geScroll container (see geSplitScroll class in baseActionEditor.h) but
+       there's nothing to scroll here actually. */
+
+       size(w(), parent()->h());
+
+       /* Draw basic boundaries (+ beat bars) and hide the unused area. Then draw 
+       children (the actions). */
+
+       baseDraw();
+
+       /* Print label. */
+
+       fl_color(G_COLOR_GREY_4);
+       fl_font(FL_HELVETICA, G_GUI_FONT_SIZE_BASE);
+       if (active())
+               fl_draw("start/stop", x() + 4, y(), w(), h(), (Fl_Align)(FL_ALIGN_LEFT | FL_ALIGN_CENTER));
+       else
+               fl_draw("start/stop (disabled)", x() + 4, y(), w(), h(), (Fl_Align)(FL_ALIGN_LEFT | FL_ALIGN_CENTER));
+
+       draw_children();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleActionEditor::onAddAction()
+{
+       Frame f = m_base->pixelToFrame(Fl::event_x() - x(), m_data->framesInBeat);
+       c::actionEditor::recordSampleAction(m_data->channelId, static_cast<gdSampleActionEditor*>(m_base)->getActionType(), f);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleActionEditor::onDeleteAction()
+{
+       c::actionEditor::deleteSampleAction(m_data->channelId, m_action->a1);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleActionEditor::onMoveAction()
+{
+       Pixel ex = Fl::event_x() - m_action->pick;
+
+       Pixel x1 = x();
+       Pixel x2 = m_base->loopWidth + x() - m_action->w();
+
+       if (ex < x1)
+               ex = x1;
+       else if (ex > x2)
+               ex = x2;
+
+       m_action->setPosition(ex);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleActionEditor::onResizeAction()
+{
+       Pixel ex = Fl::event_x();
+
+       Pixel x1 = x();
+       Pixel x2 = m_base->loopWidth + x();
+
+       if (ex < x1)
+               ex = x1;
+       else if (ex > x2)
+               ex = x2;
+
+       if (m_action->onRightEdge)
+               m_action->setRightEdge(ex - m_action->x());
+       else
+               m_action->setLeftEdge(ex);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleActionEditor::onRefreshAction()
+{
+       namespace ca = c::actionEditor;
+
+       Pixel p1   = m_action->x() - x();
+       Pixel p2   = m_action->x() + m_action->w() - x();
+       Frame f1   = 0;
+       Frame f2   = 0;
+       int   type = m_action->a1.event.getStatus();
+
+       if (!m_action->isOnEdges())
+       {
+               f1 = m_base->pixelToFrame(p1, m_data->framesInBeat);
+               f2 = m_base->pixelToFrame(p2, m_data->framesInBeat, /*snap=*/false) - (m_base->pixelToFrame(p1, m_data->framesInBeat, /*snap=*/false) - f1);
+       }
+       else if (m_action->onLeftEdge)
+       {
+               f1 = m_base->pixelToFrame(p1, m_data->framesInBeat);
+               f2 = m_action->a2.frame;
+       }
+       else if (m_action->onRightEdge)
+       {
+               f1 = m_action->a1.frame;
+               f2 = m_base->pixelToFrame(p2, m_data->framesInBeat);
+       }
+
+       ca::updateSampleAction(m_data->channelId, m_action->a1, type, f1, f2);
+
+       m_base->rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geSampleActionEditor::isNoteOffSinglePress(const m::Action& a)
+{
+       return m_data->sample->channelMode == SamplePlayerMode::SINGLE_PRESS &&
+              a.event.getStatus() == m::MidiEvent::NOTE_OFF;
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/sampleActionEditor.h b/src/gui/elems/actionEditor/sampleActionEditor.h
new file mode 100644 (file)
index 0000000..23270aa
--- /dev/null
@@ -0,0 +1,60 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SAMPLE_ACTION_EDITOR_H
+#define GE_SAMPLE_ACTION_EDITOR_H
+
+#include "baseActionEditor.h"
+
+namespace giada::m
+{
+struct Action;
+}
+
+namespace giada::v
+{
+class geSampleAction;
+class geSampleActionEditor : public geBaseActionEditor
+{
+public:
+       geSampleActionEditor(Pixel x, Pixel y, gdBaseActionEditor*);
+
+       void draw() override;
+
+       void rebuild(c::actionEditor::Data& d) override;
+
+private:
+       void onAddAction() override;
+       void onDeleteAction() override;
+       void onMoveAction() override;
+       void onResizeAction() override;
+       void onRefreshAction() override;
+
+       bool isNoteOffSinglePress(const m::Action& a);
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/splitScroll.cpp b/src/gui/elems/actionEditor/splitScroll.cpp
new file mode 100644 (file)
index 0000000..acd3a22
--- /dev/null
@@ -0,0 +1,105 @@
+/* -----------------------------------------------------------------------------
+*
+* Giada - Your Hardcore Loopmachine
+*
+* ------------------------------------------------------------------------------
+*
+* Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+*
+* This file is part of Giada - Your Hardcore Loopmachine.
+*
+* Giada - Your Hardcore Loopmachine 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.
+*
+* Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+* <http://www.gnu.org/licenses/>.
+*
+* --------------------------------------------------------------------------- */
+
+#include "splitScroll.h"
+
+namespace giada::v
+{
+geSplitScroll::geSplitScroll(Pixel x, Pixel y, Pixel w, Pixel h)
+: geSplit(x, y, w, h)
+, m_a(0, 0, 0, 0, Fl_Scroll::VERTICAL_ALWAYS)
+, m_b(0, 0, 0, 0, Direction::HORIZONTAL)
+{
+       m_b.onScrollH = [&a = m_a](Pixel x) {
+               a.scroll_to(x, a.yposition());
+       };
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSplitScroll::addWidgets(Fl_Widget& wa, Fl_Widget& wb, Pixel topContentH)
+{
+       m_a.add(&wa);
+       m_b.addWidget(&wb);
+
+       init(m_a, m_b);
+
+       if (topContentH != -1)
+               resizePanel(geSplit::Panel::A, topContentH);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Pixel geSplitScroll::getScrollX() const
+{
+       return m_b.xposition();
+}
+
+Pixel geSplitScroll::getScrollY() const
+{
+       return m_a.yposition();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Pixel geSplitScroll::getContentWidth() const
+{
+       if (m_a.countChildren() == 0)
+               return 0;
+       return m_a.child(0)->w();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Pixel geSplitScroll::getTopContentH() const
+{
+       return m_a.h();
+}
+
+/* -------------------------------------------------------------------------- */
+
+geompp::Rect<Pixel> geSplitScroll::getBoundsNoScrollbar() const
+{
+       return {
+           x(), y(),
+           w() - m_a.scrollbar.w() - G_GUI_OUTER_MARGIN,
+           h() - m_b.hscrollbar.h() - G_GUI_OUTER_MARGIN};
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSplitScroll::setScrollX(Pixel p)
+{
+       p = std::max(0, p);
+       m_a.scroll_to(p, m_a.yposition());
+       m_b.scroll_to(p, m_b.yposition());
+}
+
+void geSplitScroll::setScrollY(Pixel p)
+{
+       m_a.scroll_to(m_a.xposition(), p);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/splitScroll.h b/src/gui/elems/actionEditor/splitScroll.h
new file mode 100644 (file)
index 0000000..04f5e09
--- /dev/null
@@ -0,0 +1,59 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SPLITSCROLL_H
+#define GE_SPLITSCROLL_H
+
+#include "core/types.h"
+#include "deps/geompp/src/rect.hpp"
+#include "gui/elems/basics/liquidScroll.h"
+#include "gui/elems/basics/scroll.h"
+#include "gui/elems/basics/split.h"
+
+namespace giada::v
+{
+class geSplitScroll : public geSplit
+{
+public:
+       geSplitScroll(Pixel x, Pixel y, Pixel w, Pixel h);
+
+       Pixel               getScrollX() const;
+       Pixel               getScrollY() const;
+       Pixel               getContentWidth() const;
+       Pixel               getTopContentH() const;
+       geompp::Rect<Pixel> getBoundsNoScrollbar() const;
+
+       void addWidgets(Fl_Widget& a, Fl_Widget& b, Pixel topContentH = -1);
+       void setScrollX(Pixel p);
+       void setScrollY(Pixel p);
+
+private:
+       geScroll       m_a;
+       geLiquidScroll m_b;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/actionEditor/velocityEditor.cpp b/src/gui/elems/actionEditor/velocityEditor.cpp
new file mode 100644 (file)
index 0000000..60e9033
--- /dev/null
@@ -0,0 +1,148 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "velocityEditor.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "envelopePoint.h"
+#include "glue/actionEditor.h"
+#include "gui/dialogs/actionEditor/baseActionEditor.h"
+#include "src/core/actions/action.h"
+#include "utils/log.h"
+#include "utils/math.h"
+#include <FL/Fl.H>
+#include <FL/fl_draw.H>
+#include <cassert>
+
+namespace giada::v
+{
+geVelocityEditor::geVelocityEditor(Pixel x, Pixel y, gdBaseActionEditor* b)
+: geBaseActionEditor(x, y, 200, 40, b)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geVelocityEditor::draw()
+{
+       baseDraw();
+
+       if (h() < geEnvelopePoint::SIDE)
+               return;
+
+       /* Print label. */
+
+       fl_color(G_COLOR_GREY_4);
+       fl_font(FL_HELVETICA, G_GUI_FONT_SIZE_BASE);
+       fl_draw("Velocity", x() + 4, y(), w(), h(), FL_ALIGN_LEFT);
+
+       if (children() == 0)
+               return;
+
+       const Pixel side = geEnvelopePoint::SIDE / 2;
+
+       for (int i = 0; i < children(); i++)
+       {
+               geEnvelopePoint* p = static_cast<geEnvelopePoint*>(child(i));
+               if (m_action == nullptr)
+                       p->position(p->x(), valueToY(p->a1.event.getVelocity()));
+               const Pixel x1 = p->x() + side;
+               const Pixel y1 = p->y();
+               const Pixel y2 = y() + h();
+               fl_line(x1, y1, x1, y2);
+       }
+
+       draw_children();
+}
+
+/* -------------------------------------------------------------------------- */
+
+Pixel geVelocityEditor::valueToY(int v) const
+{
+       /* Cast the input type of 'v' to float, to make the mapping more precise. */
+       return u::math::map<float, Pixel>(v, 0, G_MAX_VELOCITY, y() + (h() - geEnvelopePoint::SIDE), y());
+}
+
+int geVelocityEditor::yToValue(Pixel px) const
+{
+       return u::math::map<Pixel, int>(px, h() - geEnvelopePoint::SIDE, 0, 0, G_MAX_VELOCITY);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geVelocityEditor::rebuild(c::actionEditor::Data& d)
+{
+       m_data = &d;
+
+       /* Remove all existing actions and set a new width, according to the current
+       zoom level. */
+
+       clear();
+       size(m_base->fullWidth, h());
+
+       for (const m::Action& action : m_data->actions)
+       {
+
+               if (action.event.getStatus() == m::MidiEvent::NOTE_OFF)
+                       continue;
+
+               Pixel px = x() + m_base->frameToPixel(action.frame);
+               Pixel py = y() + valueToY(action.event.getVelocity());
+
+               add(new geEnvelopePoint(px, py, action));
+       }
+
+       resizable(nullptr);
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geVelocityEditor::onMoveAction()
+{
+       Pixel ey = Fl::event_y() - (geEnvelopePoint::SIDE / 2);
+
+       Pixel y1 = y();
+       Pixel y2 = y() + h() - geEnvelopePoint::SIDE;
+
+       if (ey < y1)
+               ey = y1;
+       else if (ey > y2)
+               ey = y2;
+
+       m_action->position(m_action->x(), ey);
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geVelocityEditor::onRefreshAction()
+{
+       c::actionEditor::updateVelocity(m_action->a1, yToValue(m_action->y() - y()));
+
+       m_base->rebuild(); // Rebuild pianoRoll as well
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/actionEditor/velocityEditor.h b/src/gui/elems/actionEditor/velocityEditor.h
new file mode 100644 (file)
index 0000000..0ceb352
--- /dev/null
@@ -0,0 +1,56 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_VELOCITY_EDITOR_H
+#define GE_VELOCITY_EDITOR_H
+
+#include "baseActionEditor.h"
+
+namespace giada::v
+{
+class geEnvelopePoint;
+class geVelocityEditor : public geBaseActionEditor
+{
+public:
+       geVelocityEditor(Pixel x, Pixel y, gdBaseActionEditor*);
+
+       void draw() override;
+
+       void rebuild(c::actionEditor::Data& d) override;
+
+private:
+       void onMoveAction() override;
+       void onRefreshAction() override;
+       void onAddAction() override{};
+       void onDeleteAction() override{};
+       void onResizeAction() override{};
+
+       Pixel valueToY(int v) const;
+       int   yToValue(Pixel y) const;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/box.cpp b/src/gui/elems/basics/box.cpp
new file mode 100644 (file)
index 0000000..cab44a4
--- /dev/null
@@ -0,0 +1,68 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "box.h"
+#include "core/const.h"
+#include "utils/gui.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geBox::geBox(int x, int y, int w, int h, const char* l, Fl_Align al)
+: Fl_Box(x, y, w, h)
+{
+       copy_label(l);
+       box(FL_NO_BOX);
+       color(G_COLOR_GREY_1);
+       align(al | FL_ALIGN_INSIDE);
+}
+
+/* -------------------------------------------------------------------------- */
+
+geBox::geBox(const char* l, Fl_Align al)
+: geBox(0, 0, 0, 0, l, al)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBox::draw()
+{
+       fl_rectf(x(), y(), w(), h(), color()); // Clear background
+
+       if (box() != FL_NO_BOX)
+               fl_rect(x(), y(), w(), h(), G_COLOR_GREY_4); // Border
+
+       if (image() != nullptr)
+               draw_label(); // draw_label also paints image, if any
+       else if (label() != nullptr)
+       {
+               fl_color(active() ? G_COLOR_LIGHT_2 : G_COLOR_GREY_4);
+               fl_font(FL_HELVETICA, G_GUI_FONT_SIZE_BASE);
+               fl_draw(giada::u::gui::truncate(label(), w()).c_str(), x(), y(), w(), h(), align());
+       }
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/box.h b/src/gui/elems/basics/box.h
new file mode 100644 (file)
index 0000000..6cc6eda
--- /dev/null
@@ -0,0 +1,44 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_BOX_H
+#define GE_BOX_H
+
+#include <FL/Fl_Box.H>
+
+namespace giada::v
+{
+class geBox : public Fl_Box
+{
+public:
+       geBox(int x, int y, int w, int h, const char* l = nullptr, Fl_Align al = FL_ALIGN_CENTER);
+       geBox(const char* l = nullptr, Fl_Align al = FL_ALIGN_CENTER);
+
+       void draw() override;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/boxtypes.cpp b/src/gui/elems/basics/boxtypes.cpp
new file mode 100644 (file)
index 0000000..5ff93d6
--- /dev/null
@@ -0,0 +1,55 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * boxtypes
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "boxtypes.h"
+#include "../../../core/const.h"
+#include <FL/fl_draw.H>
+
+void g_customBorderBox(int x, int y, int w, int h, Fl_Color c)
+{
+       fl_color(c);
+       fl_rectf(x, y, w, h);
+       fl_color(G_COLOR_GREY_4);
+       fl_rect(x, y, w, h);
+}
+
+void g_customUpBox(int x, int y, int w, int h, Fl_Color /*c*/)
+{
+       fl_color(G_COLOR_GREY_2);
+       fl_rectf(x, y, w, h);
+       fl_color(G_COLOR_GREY_2);
+       fl_rect(x, y, w, h);
+}
+
+void g_customDownBox(int x, int y, int w, int h, Fl_Color c)
+{
+       fl_color(c);
+       fl_rectf(x, y, w, h);
+       fl_color(G_COLOR_GREY_2);
+       fl_rect(x, y, w, h);
+}
diff --git a/src/gui/elems/basics/boxtypes.h b/src/gui/elems/basics/boxtypes.h
new file mode 100644 (file)
index 0000000..2fca0ca
--- /dev/null
@@ -0,0 +1,42 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * boxtypes
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_BOXTYPES_H
+#define GE_BOXTYPES_H
+
+#include <FL/Fl.H>
+
+constexpr Fl_Boxtype G_CUSTOM_BORDER_BOX = FL_FREE_BOXTYPE;
+constexpr Fl_Boxtype G_CUSTOM_UP_BOX     = static_cast<Fl_Boxtype>(FL_FREE_BOXTYPE + 1);
+constexpr Fl_Boxtype G_CUSTOM_DOWN_BOX   = static_cast<Fl_Boxtype>(FL_FREE_BOXTYPE + 3);
+
+void g_customBorderBox(int x, int y, int w, int h, Fl_Color c);
+void g_customUpBox(int x, int y, int w, int h, Fl_Color c);
+void g_customDownBox(int x, int y, int w, int h, Fl_Color c);
+
+#endif
diff --git a/src/gui/elems/basics/browser.cpp b/src/gui/elems/basics/browser.cpp
new file mode 100644 (file)
index 0000000..5a91908
--- /dev/null
@@ -0,0 +1,62 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geScroll
+ * Custom scroll with nice scrollbars and something else.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/basics/browser.h"
+#include "core/const.h"
+#include "gui/elems/basics/boxtypes.h"
+
+namespace giada::v
+{
+geBrowser::geBrowser(int x, int y, int w, int h)
+: Fl_Browser(x, y, w, h)
+{
+       box(G_CUSTOM_BORDER_BOX);
+       textsize(G_GUI_FONT_SIZE_BASE);
+       textcolor(G_COLOR_LIGHT_2);
+       selection_color(G_COLOR_GREY_4);
+       color(G_COLOR_GREY_2);
+
+       scrollbar.color(G_COLOR_GREY_2);
+       scrollbar.selection_color(G_COLOR_GREY_4);
+       scrollbar.labelcolor(G_COLOR_LIGHT_1);
+       scrollbar.slider(G_CUSTOM_BORDER_BOX);
+
+       hscrollbar.color(G_COLOR_GREY_2);
+       hscrollbar.selection_color(G_COLOR_GREY_4);
+       hscrollbar.labelcolor(G_COLOR_LIGHT_1);
+       hscrollbar.slider(G_CUSTOM_BORDER_BOX);
+}
+
+/* -------------------------------------------------------------------------- */
+
+geBrowser::geBrowser()
+: geBrowser(0, 0, 0, 0)
+{
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/browser.h b/src/gui/elems/basics/browser.h
new file mode 100644 (file)
index 0000000..08bcde9
--- /dev/null
@@ -0,0 +1,42 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_BROWSER_H
+#define GE_BROWSER_H
+
+#include <FL/Fl_Browser.H>
+
+namespace giada::v
+{
+class geBrowser : public Fl_Browser
+{
+public:
+       geBrowser(int x, int y, int w, int h);
+       geBrowser();
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/button.cpp b/src/gui/elems/basics/button.cpp
new file mode 100644 (file)
index 0000000..f635ad9
--- /dev/null
@@ -0,0 +1,101 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "button.h"
+#include "core/const.h"
+#include "utils/gui.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geButton::geButton(int x, int y, int w, int h, const char* l,
+    const char** imgOff, const char** imgOn, const char** imgDisabled)
+: Fl_Button(x, y, w, h, l)
+, onClick(nullptr)
+, imgOff(imgOff)
+, imgOn(imgOn)
+, imgDisabled(imgDisabled)
+, bgColor0(G_COLOR_GREY_2)
+, bgColor1(G_COLOR_GREY_4)
+, bdColor(G_COLOR_GREY_4)
+, txtColor(G_COLOR_LIGHT_2)
+{
+       callback(cb_click);
+}
+
+/* -------------------------------------------------------------------------- */
+
+geButton::geButton(const char* l, const char** imgOff, const char** imgOn, const char** imgDisabled)
+: geButton(0, 0, 0, 0, l, imgOff, imgOn, imgDisabled)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geButton::cb_click(Fl_Widget* w, void*)
+{
+       geButton* b = static_cast<geButton*>(w);
+       if (b->onClick != nullptr)
+               b->onClick();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geButton::draw()
+{
+       //Fl_Button::draw();
+
+       if (active())
+               if (value())
+                       draw(imgOn, bgColor1, txtColor);
+               else
+                       draw(imgOff, bgColor0, txtColor);
+       else
+               draw(imgDisabled, bgColor0, bdColor);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geButton::draw(const char** img, Fl_Color bgColor, Fl_Color textColor)
+{
+       fl_rect(x(), y(), w(), h(), bdColor); // draw border
+
+       if (img != nullptr)
+       {
+               fl_draw_pixmap(img, x() + 1, y() + 1);
+               return;
+       }
+
+       fl_rectf(x() + 1, y() + 1, w() - 2, h() - 2, bgColor); // draw background
+       fl_color(textColor);
+
+       if (label() != nullptr)
+       {
+               fl_font(FL_HELVETICA, G_GUI_FONT_SIZE_BASE);
+               fl_draw(giada::u::gui::truncate(label(), w() - 16).c_str(), x() + 2, y(), w() - 2, h(), FL_ALIGN_CENTER);
+       }
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/button.h b/src/gui/elems/basics/button.h
new file mode 100644 (file)
index 0000000..5bb7ec7
--- /dev/null
@@ -0,0 +1,68 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geButton
+ * A regular button.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_BUTTON_H
+#define GE_BUTTON_H
+
+#include <FL/Fl_Button.H>
+#include <functional>
+
+namespace giada::v
+{
+class geButton : public Fl_Button
+{
+public:
+       geButton(int x, int y, int w, int h, const char* l = nullptr,
+           const char** imgOff = nullptr, const char** imgOn = nullptr,
+           const char** imgDisabled = nullptr);
+
+       geButton(const char* l = nullptr, const char** imgOff = nullptr,
+           const char** imgOn = nullptr, const char** imgDisabled = nullptr);
+
+       void draw() override;
+
+       std::function<void()> onClick;
+
+protected:
+       static void cb_click(Fl_Widget*, void*);
+
+       void draw(const char** img, Fl_Color bgColor, Fl_Color textColor);
+
+       const char** imgOff;
+       const char** imgOn;
+       const char** imgDisabled;
+
+       Fl_Color bgColor0; // background not clicked
+       Fl_Color bgColor1; // background clicked
+       Fl_Color bdColor;  // border
+       Fl_Color txtColor; // text
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/check.cpp b/src/gui/elems/basics/check.cpp
new file mode 100644 (file)
index 0000000..b13b5d9
--- /dev/null
@@ -0,0 +1,75 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "check.h"
+#include "core/const.h"
+#include <FL/fl_draw.H>
+#include <cstring>
+
+geCheck::geCheck(int x, int y, int w, int h, const char* l)
+: Fl_Check_Button(x, y, w, h, l)
+{
+       callback(cb_onChange, this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geCheck::cb_onChange(Fl_Widget* /*w*/, void* p) { (static_cast<geCheck*>(p))->cb_onChange(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geCheck::cb_onChange()
+{
+       if (onChange != nullptr)
+               onChange(value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geCheck::draw()
+{
+       fl_rectf(x(), y(), w(), h(), FL_BACKGROUND_COLOR); // clearer
+
+       const Fl_Color boxColor  = !active() ? FL_INACTIVE_COLOR : G_COLOR_GREY_4;
+       const int      textColor = !active() ? FL_INACTIVE_COLOR : G_COLOR_LIGHT_2;
+       const Fl_Align textAlign = hasMultilineText() ? FL_ALIGN_LEFT | FL_ALIGN_TOP : FL_ALIGN_LEFT | FL_ALIGN_CENTER;
+
+       if (value())
+               fl_rectf(x(), y(), 12, h(), boxColor);
+       else
+               fl_rect(x(), y(), 12, h(), boxColor);
+
+       fl_font(FL_HELVETICA, G_GUI_FONT_SIZE_BASE);
+       fl_color(textColor);
+       fl_draw(label(), x() + 20, y(), w(), h(), textAlign);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geCheck::hasMultilineText() const
+{
+       return label() == nullptr ? false : std::strchr(label(), '\n') != nullptr;
+}
diff --git a/src/gui/elems/basics/check.h b/src/gui/elems/basics/check.h
new file mode 100644 (file)
index 0000000..3178a49
--- /dev/null
@@ -0,0 +1,49 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_CHECK_H
+#define GE_CHECK_H
+
+#include <FL/Fl_Check_Button.H>
+#include <functional>
+
+class geCheck : public Fl_Check_Button
+{
+public:
+       geCheck(int x, int y, int w, int h, const char* l = 0);
+
+       void draw() override;
+
+       std::function<void(bool)> onChange = nullptr;
+
+private:
+       static void cb_onChange(Fl_Widget* w, void* p);
+       void        cb_onChange();
+
+       bool hasMultilineText() const;
+};
+
+#endif
diff --git a/src/gui/elems/basics/choice.cpp b/src/gui/elems/basics/choice.cpp
new file mode 100644 (file)
index 0000000..c2a41e9
--- /dev/null
@@ -0,0 +1,170 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/basics/choice.h"
+#include "core/const.h"
+#include "gui/drawing.h"
+#include "utils/gui.h"
+#include "utils/vector.h"
+#include <FL/fl_draw.H>
+#include <cassert>
+
+namespace giada::v
+{
+geChoice::geMenu::geMenu(int x, int y, int w, int h)
+: Fl_Choice(x, y, w, h)
+{
+       labelsize(G_GUI_FONT_SIZE_BASE);
+       labelcolor(G_COLOR_LIGHT_2);
+       box(FL_BORDER_BOX);
+       textsize(G_GUI_FONT_SIZE_BASE);
+       textcolor(G_COLOR_LIGHT_2);
+       color(G_COLOR_GREY_2);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChoice::geMenu::draw()
+{
+       geompp::Rect<int> bounds(x(), y(), w(), h());
+
+       drawRectf(bounds, G_COLOR_GREY_2);                       // background
+       drawRect(bounds, static_cast<Fl_Color>(G_COLOR_GREY_4)); // border
+       fl_polygon(x() + w() - 8, y() + h() - 1, x() + w() - 1, y() + h() - 8, x() + w() - 1, y() + h() - 1);
+       if (value() != -1)
+               drawText(text(value()), bounds, active() ? G_COLOR_LIGHT_2 : G_COLOR_GREY_4);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geChoice::geChoice(int x, int y, int w, int h, const char* l, int labelWidth)
+: geFlex(x, y, w, h, Direction::HORIZONTAL, G_GUI_INNER_MARGIN)
+, m_text(nullptr)
+, m_menu(nullptr)
+{
+       if (l != nullptr)
+       {
+               m_text = new geBox(l, FL_ALIGN_RIGHT);
+               add(m_text, labelWidth != 0 ? labelWidth : u::gui::getStringRect(l).w);
+       }
+       m_menu = new geMenu(x, y, w, h);
+       add(m_menu);
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+geChoice::geChoice(const char* l, int labelWidth)
+: geChoice(0, 0, 0, 0, l, labelWidth)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChoice::cb_onChange(Fl_Widget* /*w*/, void* p) { (static_cast<geChoice*>(p))->cb_onChange(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geChoice::cb_onChange()
+{
+       if (onChange != nullptr)
+               onChange(getSelectedId());
+}
+
+/* -------------------------------------------------------------------------- */
+
+ID geChoice::getSelectedId() const
+{
+       return m_menu->value() == -1 ? -1 : m_ids.at(m_menu->value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChoice::addItem(const std::string& label, ID id)
+{
+       m_menu->add(label.c_str(), 0, cb_onChange, static_cast<void*>(this));
+
+       if (id != -1)
+               m_ids.push_back(id);
+       else // auto-increment
+               m_ids.push_back(m_ids.size() == 0 ? 0 : m_ids.back() + 1);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChoice::showItem(const std::string& label)
+{
+       m_menu->value(m_menu->find_index(label.c_str()));
+}
+
+void geChoice::showItem(ID id)
+{
+       m_menu->value(u::vector::indexOf(m_ids, id));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChoice::activate()
+{
+       geFlex::activate();
+       m_menu->activate();
+       if (m_text != nullptr)
+               m_text->activate();
+}
+
+void geChoice::deactivate()
+{
+       geFlex::deactivate();
+       m_menu->deactivate();
+       if (m_text != nullptr)
+               m_text->deactivate();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string geChoice::getSelectedLabel() const
+{
+       return m_menu->text();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::size_t geChoice::countItems() const
+{
+       return m_ids.size();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChoice::clear()
+{
+       m_menu->clear();
+       m_ids.clear();
+}
+
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/choice.h b/src/gui/elems/basics/choice.h
new file mode 100644 (file)
index 0000000..f96feac
--- /dev/null
@@ -0,0 +1,86 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_CHOICE_H
+#define GE_CHOICE_H
+
+#include "core/types.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/flex.h"
+#include <FL/Fl_Choice.H>
+#include <functional>
+#include <string>
+#include <vector>
+
+namespace giada::v
+{
+class geChoice : public geFlex
+{
+public:
+       /* geChoice 
+       Constructors. If label is != nullptr but labelWidth is not specified, the
+       label width is automatically computed and adjusted accordingly. */
+
+       geChoice(int x, int y, int w, int h, const char* l = nullptr, int labelWidth = 0);
+       geChoice(const char* l = nullptr, int labelWidth = 0);
+
+       ID          getSelectedId() const;
+       std::string getSelectedLabel() const;
+       std::size_t countItems() const;
+
+       /* addItem
+       Adds a new item with a certain ID. Pass id = -1 to auto-increment it (ID
+       starts from 0). */
+
+       void addItem(const std::string& label, ID id = -1);
+
+       void showItem(const std::string& label);
+       void showItem(ID);
+       void activate();
+       void deactivate();
+
+       void clear();
+
+       std::function<void(ID)> onChange = nullptr;
+
+private:
+       class geMenu : public Fl_Choice
+       {
+       public:
+               geMenu(int x, int y, int w, int h);
+               void draw() override;
+       };
+
+       static void cb_onChange(Fl_Widget* w, void* p);
+       void        cb_onChange();
+
+       geBox*          m_text;
+       geMenu*         m_menu;
+       std::vector<ID> m_ids;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/dial.cpp b/src/gui/elems/basics/dial.cpp
new file mode 100644 (file)
index 0000000..7f11152
--- /dev/null
@@ -0,0 +1,69 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/basics/dial.h"
+#include "core/const.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geDial::geDial(int x, int y, int w, int h, const char* l)
+: Fl_Dial(x, y, w, h, l)
+, onChange(nullptr)
+{
+       labelsize(G_GUI_FONT_SIZE_BASE);
+       labelcolor(G_COLOR_LIGHT_2);
+       align(FL_ALIGN_LEFT);
+       type(FL_FILL_DIAL);
+       angles(0, 360);
+       color(G_COLOR_GREY_2);           // background
+       selection_color(G_COLOR_GREY_4); // selection
+       callback(cb_change);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geDial::cb_change(Fl_Widget* w, void*)
+{
+       geDial* d = static_cast<geDial*>(w);
+       if (d->onChange != nullptr)
+               d->onChange(d->value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geDial::draw()
+{
+       double angle = (angle2() - angle1()) * (value() - minimum()) / (maximum() - minimum()) + angle1();
+
+       fl_color(G_COLOR_GREY_2);
+       fl_pie(x(), y(), w(), h(), 270 - angle1(), angle > angle1() ? 360 + 270 - angle : 270 - 360 - angle);
+
+       fl_color(G_COLOR_GREY_4);
+       fl_arc(x(), y(), w(), h(), 0, 360);
+       fl_pie(x(), y(), w(), h(), 270 - angle, 270 - angle1());
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/dial.h b/src/gui/elems/basics/dial.h
new file mode 100644 (file)
index 0000000..f33d8db
--- /dev/null
@@ -0,0 +1,49 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_DIAL_H
+#define GE_DIAL_H
+
+#include <FL/Fl_Dial.H>
+#include <functional>
+
+namespace giada::v
+{
+class geDial : public Fl_Dial
+{
+public:
+       geDial(int x, int y, int w, int h, const char* l = 0);
+
+       void draw() override;
+
+       std::function<void(float)> onChange;
+
+private:
+       static void cb_change(Fl_Widget*, void*);
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/flex.cpp b/src/gui/elems/basics/flex.cpp
new file mode 100644 (file)
index 0000000..070be45
--- /dev/null
@@ -0,0 +1,163 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "flex.h"
+#include <cstddef>
+#include <numeric>
+
+namespace giada::v
+{
+geFlex::Elem::Elem(Fl_Widget& w, geFlex& parent, Direction d, int size, geompp::Border<int> pad)
+: size(size)
+, m_w(w)
+, m_parent(parent)
+, m_dir(d)
+, m_pad(pad)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geFlex::Elem::getSize() const
+{
+       if (isFixed())
+               return size;
+       return m_dir == Direction::VERTICAL ? m_w.h() : m_w.w();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geFlex::Elem::isFixed() const
+{
+       return size != -1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geFlex::Elem::resize(int pos, int newSize)
+{
+       geompp::Rect<int> bounds;
+
+       if (m_dir == Direction::VERTICAL)
+               bounds = geompp::Rect<int>(m_parent.x(), pos, m_parent.w(), newSize).reduced(m_pad);
+       else
+               bounds = geompp::Rect<int>(pos, m_parent.y(), newSize, m_parent.h()).reduced(m_pad);
+
+       m_w.resize(bounds.x, bounds.y, bounds.w, bounds.h);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geFlex::geFlex(int x, int y, int w, int h, Direction d, int gutter)
+: Fl_Group(x, y, w, h, 0)
+, m_direction(d)
+, m_gutter(gutter)
+, m_numFixed(0)
+{
+       Fl_Group::end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+geFlex::geFlex(geompp::Rect<int> r, Direction d, int gutter)
+: geFlex(r.x, r.y, r.w, r.h, d, gutter)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+geFlex::geFlex(Direction d, int gutter)
+: geFlex(0, 0, 0, 0, d, gutter)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geFlex::add(Fl_Widget& w, int size, geompp::Border<int> pad)
+{
+       Fl_Group::add(w);
+       m_elems.push_back({w, *this, m_direction, size, pad});
+       if (size != -1)
+               m_numFixed++;
+}
+
+void geFlex::add(Fl_Widget* w, int size, geompp::Border<int> pad)
+{
+       geFlex::add(*w, size, pad);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geFlex::resize(int X, int Y, int W, int H)
+{
+       Fl_Group::resize(X, Y, W, H);
+
+       const size_t numAllElems    = m_elems.size();
+       const size_t numLiquidElems = numAllElems - m_numFixed;
+
+       const int pos  = m_direction == Direction::VERTICAL ? y() : x();
+       const int size = m_direction == Direction::VERTICAL ? h() : w();
+
+       /* No fancy computations if there are no liquid elements. Just lay children
+       according to their fixed size. */
+
+       if (numLiquidElems == 0)
+       {
+               layWidgets(pos);
+               return;
+       }
+
+       const int fixedElemsSize = std::accumulate(m_elems.begin(), m_elems.end(), 0, [](int acc, const Elem& e) {
+               return e.isFixed() ? acc + e.getSize() : acc;
+       });
+       const int availableSize  = size - (m_gutter * (numAllElems - 1)); // Total size - gutters
+       const int liquidElemSize = (availableSize - fixedElemsSize) / numLiquidElems;
+
+       layWidgets(pos, liquidElemSize);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geFlex::layWidgets(int startPos, int sizeIfLiquid)
+{
+       int nextElemPos = startPos;
+       for (Elem& e : m_elems)
+       {
+               e.resize(nextElemPos, e.isFixed() ? e.size : sizeIfLiquid);
+               nextElemPos += e.getSize() + m_gutter;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geFlex::end()
+{
+       Fl_Group::end();
+       resize(x(), y(), w(), h());
+}
+} // namespace giada::v
diff --git a/src/gui/elems/basics/flex.h b/src/gui/elems/basics/flex.h
new file mode 100644 (file)
index 0000000..574409e
--- /dev/null
@@ -0,0 +1,93 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_FLEX_H
+#define GE_FLEX_H
+
+#include "deps/geompp/src/border.hpp"
+#include "deps/geompp/src/rect.hpp"
+#include "gui/types.h"
+#include <FL/Fl_Group.H>
+#include <vector>
+
+namespace giada::v
+{
+/* geFlex
+Like a FlexBox item, it's a group that contains widgets that can be stretched 
+to fill the area. Inspired by https://github.com/osen/FL_Flex. */
+
+class geFlex : public Fl_Group
+{
+public:
+       geFlex(int x, int y, int w, int h, Direction d, int gutter = 0);
+       geFlex(geompp::Rect<int>, Direction d, int gutter = 0);
+       geFlex(Direction d, int gutter = 0);
+
+       /* add
+       Adds an existing widget to the Flex layout. If 'size' == -1, the widget
+       will be stretched to take up the available space. WARNING: like Fl_Group,
+       geFlex owns widgets! */
+
+       void add(Fl_Widget&, int size = -1, geompp::Border<int> pad = {});
+       void add(Fl_Widget*, int size = -1, geompp::Border<int> pad = {});
+
+       /* end
+       Finalize the Flex item. Call this when you're done add()ing widgets. */
+
+       void end();
+
+private:
+       class Elem
+       {
+       public:
+               Elem(Fl_Widget&, geFlex& parent, Direction, int size, geompp::Border<int> pad);
+
+               int  getSize() const;
+               bool isFixed() const;
+
+               void resize(int pos, int size);
+
+               int size;
+
+       private:
+               Fl_Widget&          m_w;
+               geFlex&             m_parent;
+               Direction           m_dir;
+               geompp::Border<int> m_pad;
+       };
+
+       void resize(int x, int y, int w, int h) override;
+
+       void layWidgets(int startPos, int sizeIfLiquid = 0);
+
+       Direction         m_direction;
+       int               m_gutter;
+       std::vector<Elem> m_elems;
+       int               m_numFixed;
+};
+} // namespace giada::v
+
+#endif
\ No newline at end of file
diff --git a/src/gui/elems/basics/group.cpp b/src/gui/elems/basics/group.cpp
new file mode 100644 (file)
index 0000000..fc50cd2
--- /dev/null
@@ -0,0 +1,90 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geScroll
+ * Custom scroll with nice scrollbars and something else.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "group.h"
+#include <cstddef>
+#include <algorithm>
+
+namespace giada
+{
+namespace v
+{
+geGroup::geGroup(int x, int y)
+: Fl_Group(x, y, 0, 0)
+{
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::size_t geGroup::countChildren() const
+{
+       return m_widgets.size();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geGroup::add(Fl_Widget* widget)
+{
+       widget->position(widget->x() + x(), widget->y() + y());
+
+       Fl_Group::add(widget);
+       m_widgets.push_back(widget);
+
+       int newW = 0;
+       int newH = 0;
+
+       for (const Fl_Widget* wg : m_widgets)
+       {
+               newW = std::max(newW, (wg->x() + wg->w()) - x());
+               newH = std::max(newH, (wg->y() + wg->h()) - y());
+       }
+
+       /* Don't call size(newW, newH) as it changes widgets position. Adjust width
+    and height manually instead. */
+
+       w(newW);
+       h(newH);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Fl_Widget* geGroup::getChild(std::size_t i)
+{
+       return m_widgets.at(i); // Throws std::out_of_range in case
+}
+
+/* -------------------------------------------------------------------------- */
+
+Fl_Widget* geGroup::getLastChild()
+{
+       return m_widgets.at(m_widgets.size() - 1); // Throws std::out_of_range in case
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/basics/group.h b/src/gui/elems/basics/group.h
new file mode 100644 (file)
index 0000000..1460911
--- /dev/null
@@ -0,0 +1,77 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geScroll
+ * Custom scroll with nice scrollbars and something else.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_GROUP_H
+#define GE_GROUP_H
+
+#include <FL/Fl_Group.H>
+#include <cstddef>
+#include <vector>
+
+namespace giada
+{
+namespace v
+{
+/* geGroup
+A group that resizes itself according to the content. */
+
+class geGroup : public Fl_Group
+{
+public:
+       geGroup(int x, int y);
+
+       /* countChildren
+    Returns the number of widgets contained in this group. */
+
+       std::size_t countChildren() const;
+
+       /* add
+    Adds a Fl_Widget 'w' to this group. Coordinates are relative to the group,
+    so origin starts at (0, 0). As with any other FLTK group, the widget becomes
+       owned by this group: If you add static or automatic (local) variables, then 
+       it is your responsibility to remove (or delete) all such static or automatic
+       child widgets before destroying the group - otherwise the child widgets' 
+       destructors would be called twice! */
+
+       void add(Fl_Widget* w);
+
+       Fl_Widget* getChild(std::size_t i);
+       Fl_Widget* getLastChild();
+
+private:
+       /* m_widgets 
+    The internal Fl_Scroll::array_ is unreliable when inspected with the child()
+    method. Let's keep track of widgets that belong to this group manually. */
+
+       std::vector<Fl_Widget*> m_widgets;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/elems/basics/input.cpp b/src/gui/elems/basics/input.cpp
new file mode 100644 (file)
index 0000000..0fc1eae
--- /dev/null
@@ -0,0 +1,58 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "input.h"
+#include "boxtypes.h"
+#include "core/const.h"
+
+geInput::geInput(int x, int y, int w, int h, const char* l)
+: Fl_Input(x, y, w, h, l)
+{
+       //Fl::set_boxtype(G_CUSTOM_BORDER_BOX, gDrawBox, 1, 1, 2, 2);
+       box(G_CUSTOM_BORDER_BOX);
+       labelsize(G_GUI_FONT_SIZE_BASE);
+       labelcolor(G_COLOR_LIGHT_2);
+       color(G_COLOR_BLACK);
+       textcolor(G_COLOR_LIGHT_2);
+       cursor_color(G_COLOR_LIGHT_2);
+       selection_color(G_COLOR_GREY_4);
+       textsize(G_GUI_FONT_SIZE_BASE);
+
+       when(FL_WHEN_CHANGED);
+       callback(cb_onChange, this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geInput::cb_onChange(Fl_Widget* /*w*/, void* p) { (static_cast<geInput*>(p))->cb_onChange(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geInput::cb_onChange()
+{
+       if (onChange != nullptr)
+               onChange(value());
+}
\ No newline at end of file
diff --git a/src/gui/elems/basics/input.h b/src/gui/elems/basics/input.h
new file mode 100644 (file)
index 0000000..59307ff
--- /dev/null
@@ -0,0 +1,46 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_INPUT_H
+#define GE_INPUT_H
+
+#include <FL/Fl_Input.H>
+#include <functional>
+#include <string>
+
+class geInput : public Fl_Input
+{
+public:
+       geInput(int x, int y, int w, int h, const char* l = 0);
+
+       std::function<void(const std::string&)> onChange = nullptr;
+
+private:
+       static void cb_onChange(Fl_Widget* w, void* p);
+       void        cb_onChange();
+};
+
+#endif
diff --git a/src/gui/elems/basics/liquidScroll.cpp b/src/gui/elems/basics/liquidScroll.cpp
new file mode 100644 (file)
index 0000000..2544c0d
--- /dev/null
@@ -0,0 +1,67 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * gLiquidScroll
+ * custom scroll that tells children to follow scroll's width when
+ * resized. Thanks to Greg Ercolano from FLTK dev team.
+ * http://seriss.com/people/erco/fltk/
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "liquidScroll.h"
+#include "boxtypes.h"
+#include "core/const.h"
+
+namespace giada::v
+{
+geLiquidScroll::geLiquidScroll(int x, int y, int w, int h, Direction d)
+: geScroll(x, y, w, h, d == Direction::VERTICAL ? Fl_Scroll::VERTICAL_ALWAYS : Fl_Scroll::HORIZONTAL_ALWAYS)
+, m_direction(d)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+geLiquidScroll::geLiquidScroll(geompp::Rect<int> r, Direction d)
+: geLiquidScroll(r.x, r.y, r.w, r.h, d)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geLiquidScroll::resize(int X, int Y, int W, int H)
+{
+       const int nc = children() - 2; // skip hscrollbar and vscrollbar
+       for (int t = 0; t < nc; t++)   // tell children to resize to our new width
+       {
+               Fl_Widget* c = child(t);
+               if (m_direction == Direction::VERTICAL)
+                       c->resize(c->x(), c->y(), W - 24, c->h()); // -24: leave room for scrollbar
+               else
+                       c->resize(c->x(), c->y(), c->w(), H - 24); // -24: leave room for scrollbar
+       }
+       init_sizes(); // tell scroll children changed in size
+       Fl_Scroll::resize(X, Y, W, H);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/liquidScroll.h b/src/gui/elems/basics/liquidScroll.h
new file mode 100644 (file)
index 0000000..f56e78b
--- /dev/null
@@ -0,0 +1,75 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * gLiquidScroll
+ * custom scroll that tells children to follow scroll's width when
+ * resized. Thanks to Greg Ercolano from FLTK dev team.
+ * http://seriss.com/people/erco/fltk/
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_LIQUID_SCROLL_H
+#define GE_LIQUID_SCROLL_H
+
+#include "core/const.h"
+#include "deps/geompp/src/rect.hpp"
+#include "gui/types.h"
+#include "scroll.h"
+
+namespace giada::v
+{
+class geLiquidScroll : public geScroll
+{
+public:
+       geLiquidScroll(int x, int y, int w, int h, Direction d);
+       geLiquidScroll(geompp::Rect<int>, Direction d);
+
+       void resize(int x, int y, int w, int h) override;
+
+       /* addWidget
+    Adds a new widget to the bottom, with proper spacing. */
+
+       template <typename T>
+       T* addWidget(T* wg)
+       {
+               int numChildren = countChildren();
+
+               int wx = x();
+               int wy = y() - yposition() + (numChildren * (wg->h() + G_GUI_INNER_MARGIN));
+               int ww = w() - 24;
+               int wh = wg->h();
+
+               wg->resize(wx, wy, ww, wh);
+               add(wg);
+               redraw();
+
+               return wg;
+       }
+
+private:
+       Direction m_direction;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/pack.cpp b/src/gui/elems/basics/pack.cpp
new file mode 100644 (file)
index 0000000..c4d5446
--- /dev/null
@@ -0,0 +1,56 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geScroll
+ * Custom scroll with nice scrollbars and something else.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "pack.h"
+#include "core/const.h"
+
+namespace giada::v
+{
+gePack::gePack(int x, int y, Direction d, int gutter)
+: geGroup(x, y)
+, m_direction(d)
+, m_gutter(gutter)
+{
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePack::add(Fl_Widget* widget)
+{
+       if (countChildren() == 0)
+               widget->position(0, 0);
+       else if (m_direction == Direction::HORIZONTAL)
+               widget->position((getLastChild()->x() + getLastChild()->w() + m_gutter) - x(), 0);
+       else
+               widget->position(0, (getLastChild()->y() + getLastChild()->h() + m_gutter) - y());
+
+       geGroup::add(widget);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/pack.h b/src/gui/elems/basics/pack.h
new file mode 100644 (file)
index 0000000..10c853a
--- /dev/null
@@ -0,0 +1,63 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geScroll
+ * Custom scroll with nice scrollbars and something else.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_PACK_H
+#define GE_PACK_H
+
+#include "core/const.h"
+#include "gui/elems/basics/group.h"
+#include "gui/types.h"
+
+namespace giada::v
+{
+/* gePack
+A stack of widgets that resize itself according to its content. */
+
+class gePack : public geGroup
+{
+public:
+       gePack(int x, int y, Direction d, int gutter = G_GUI_INNER_MARGIN);
+
+       /* add
+    Adds a Fl_Widget 'w' to this pack. Coordinates are relative to the group,
+    so origin starts at (0, 0). As with any other FLTK group, the widget becomes
+       owned by this group: If you add static or automatic (local) variables, then 
+       it is your responsibility to remove (or delete) all such static or automatic
+       child widgets before destroying the group - otherwise the child widgets' 
+       destructors would be called twice! */
+
+       void add(Fl_Widget* w);
+
+private:
+       Direction m_direction;
+       int       m_gutter;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/progress.cpp b/src/gui/elems/basics/progress.cpp
new file mode 100644 (file)
index 0000000..d1066f4
--- /dev/null
@@ -0,0 +1,39 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/basics/progress.h"
+#include "core/const.h"
+#include "gui/elems/basics/boxtypes.h"
+
+namespace giada::v
+{
+geProgress::geProgress(int x, int y, int w, int h, const char* l)
+: Fl_Progress(x, y, w, h, l)
+{
+       color(G_COLOR_GREY_2, G_COLOR_GREY_4);
+       box(G_CUSTOM_BORDER_BOX);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/progress.h b/src/gui/elems/basics/progress.h
new file mode 100644 (file)
index 0000000..7ddea0d
--- /dev/null
@@ -0,0 +1,41 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_PROGRESS_H
+#define GE_PROGRESS_H
+
+#include <FL/Fl_Progress.H>
+
+namespace giada::v
+{
+class geProgress : public Fl_Progress
+{
+public:
+       geProgress(int x, int y, int w, int h, const char* l = 0);
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/resizerBar.cpp b/src/gui/elems/basics/resizerBar.cpp
new file mode 100644 (file)
index 0000000..8ec0827
--- /dev/null
@@ -0,0 +1,233 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "resizerBar.h"
+#include "core/const.h"
+#include <FL/Fl.H>
+#include <FL/Fl_Group.H>
+#include <FL/fl_draw.H>
+#include <cassert>
+#include <cstddef>
+#include <vector>
+
+namespace giada::v
+{
+geResizerBar::geResizerBar(int X, int Y, int W, int H, int minSize, Direction dir, Mode mode)
+: Fl_Box(X, Y, W, H)
+, m_direction(dir)
+, m_mode(mode)
+, m_minSize(minSize)
+, m_lastPos(0)
+, m_hover(false)
+{
+       visible_focus(0);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geResizerBar::handleDrag(int diff)
+{
+       m_mode == Mode::MOVE ? move(diff) : resize(diff);
+
+       Fl_Group* group = static_cast<Fl_Group*>(parent());
+       group->init_sizes();
+       group->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geResizerBar::move(int diff)
+{
+       Fl_Widget&              wfirst  = getFirstWidget();
+       std::vector<Fl_Widget*> wothers = findWidgets([this](const Fl_Widget& wd) { return isAfter(wd); });
+
+       if (m_direction == Direction::VERTICAL)
+       {
+               if (wfirst.h() + diff < m_minSize)
+                       diff = 0;
+               wfirst.resize(wfirst.x(), wfirst.y(), wfirst.w(), wfirst.h() + diff);
+               for (Fl_Widget* wd : wothers)
+                       wd->resize(wd->x(), wd->y() + diff, wd->w(), wd->h());
+               resize(x(), y() + diff, w(), h());
+       }
+       else if (m_direction == Direction::HORIZONTAL)
+       {
+               if (wfirst.w() + diff < m_minSize)
+                       diff = 0;
+               wfirst.resize(wfirst.x(), wfirst.y(), wfirst.w() + diff, wfirst.h());
+               for (Fl_Widget* wd : wothers)
+                       wd->resize(wd->x() + diff, wd->y(), wd->w(), wd->h());
+               resize(x() + diff, y(), w(), h());
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geResizerBar::resize(int diff)
+{
+       Fl_Widget& wa = getFirstWidget();
+       Fl_Widget& wb = *findWidgets([this](const Fl_Widget& wd) { return isAfter(wd); }, /*howmany=*/1)[0];
+
+       if (m_direction == Direction::VERTICAL)
+       {
+               if (wa.h() + diff < m_minSize || wb.h() - diff < m_minSize)
+                       diff = 0;
+               wa.resize(wa.x(), wa.y(), wa.w(), wa.h() + diff);
+               wb.resize(wb.x(), wb.y() + diff, wb.w(), wb.h() - diff);
+               resize(x(), y() + diff, w(), h());
+       }
+       else if (m_direction == Direction::HORIZONTAL)
+       {
+               if (wa.w() + diff < m_minSize || wb.w() - diff < m_minSize)
+                       diff = 0;
+               wa.resize(wa.x(), wa.y(), wa.w() + diff, wa.h());
+               wb.resize(wb.x() + diff, wb.y(), wb.w() - diff, wb.h());
+               resize(x() + diff, y(), w(), h());
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geResizerBar::isBefore(const Fl_Widget& wd) const
+{
+       const int before = m_direction == Direction::VERTICAL ? y() : x();
+       return (m_direction == Direction::VERTICAL && wd.y() + wd.h() == before) ||
+              (m_direction == Direction::HORIZONTAL && wd.x() + wd.w() == before);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geResizerBar::isAfter(const Fl_Widget& wd) const
+{
+       const int after = m_direction == Direction::VERTICAL ? y() + h() : x() + w();
+       return (m_direction == Direction::VERTICAL && wd.y() >= after) ||
+              (m_direction == Direction::HORIZONTAL && wd.x() >= after);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Fl_Widget& geResizerBar::getFirstWidget()
+{
+       return *findWidgets([this](const Fl_Widget& wd) { return isBefore(wd); }, /*howmany=*/1)[0];
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<Fl_Widget*> geResizerBar::findWidgets(std::function<bool(const Fl_Widget&)> f, int howmany) const
+{
+       std::vector<Fl_Widget*> out;
+       Fl_Group*               group = static_cast<Fl_Group*>(parent());
+
+       for (int t = 0; t < group->children(); t++)
+       {
+               Fl_Widget* wd = group->child(t);
+               if (!f(*wd))
+                       continue;
+               out.push_back(wd);
+               if (howmany != -1 && out.size() == (size_t)howmany)
+                       break;
+       }
+
+       /* Make sure it finds the exact number of widgets requested, in case 
+       howmany != -1. */
+
+       assert(howmany == -1 || (howmany != -1 && out.size() == (size_t)howmany));
+
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geResizerBar::draw()
+{
+       Fl_Box::draw();
+       fl_rectf(x(), y(), w(), h(), m_hover ? G_COLOR_GREY_2 : G_COLOR_GREY_1);
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geResizerBar::handle(int e)
+{
+       int ret        = 0;
+       int currentPos = m_direction == Direction::VERTICAL ? Fl::event_y_root() : Fl::event_x_root();
+
+       switch (e)
+       {
+       case FL_FOCUS:
+               ret = 1;
+               break;
+       case FL_ENTER:
+               ret = 1;
+               fl_cursor(m_direction == Direction::VERTICAL ? FL_CURSOR_NS : FL_CURSOR_WE);
+               m_hover = true;
+               redraw();
+               break;
+       case FL_LEAVE:
+               ret = 1;
+               fl_cursor(FL_CURSOR_DEFAULT);
+               m_hover = false;
+               redraw();
+               break;
+       case FL_PUSH:
+               ret       = 1;
+               m_lastPos = currentPos;
+               break;
+       case FL_DRAG:
+               handleDrag(currentPos - m_lastPos);
+               m_lastPos = currentPos;
+               ret       = 1;
+               if (onDrag != nullptr)
+                       onDrag(getFirstWidget());
+               break;
+       case FL_RELEASE:
+               if (onRelease != nullptr)
+                       onRelease(getFirstWidget());
+               break;
+       default:
+               break;
+       }
+       return (Fl_Box::handle(e) | ret);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geResizerBar::resize(int X, int Y, int W, int H)
+{
+       if (m_direction == Direction::VERTICAL)
+               Fl_Box::resize(X, Y, W, h());
+       else
+               Fl_Box::resize(X, Y, w(), H);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geResizerBar::moveTo(int p)
+{
+       const Fl_Widget& wd   = getFirstWidget();
+       const int        curr = m_direction == Direction::VERTICAL ? wd.h() : wd.w();
+       handleDrag(p - curr);
+}
+} // namespace giada::v
diff --git a/src/gui/elems/basics/resizerBar.h b/src/gui/elems/basics/resizerBar.h
new file mode 100644 (file)
index 0000000..31057fb
--- /dev/null
@@ -0,0 +1,122 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_RESIZER_BAR_H
+#define GE_RESIZER_BAR_H
+
+#include <FL/Fl_Box.H>
+#include <functional>
+
+/* geResizerBar
+A 'resizer bar' between widgets inside a Fl_Scroll. Thanks to Greg Ercolano from
+FLTK dev team (http://seriss.com/people/erco/fltk/). It also shows a resize 
+cursor when hovered over.
+
+Assumes:
+       - Parent is an Fl_Group;
+       - The widget before us has an edge touching our edge;
+         ie. w->y() + w->h() == this->y() if Direction::VERTICAL.
+
+When this widget is dragged:
+       - The widget before us (with a common edge) will be resized;
+       - if Mode == MOVE 
+               All children after us will be moved.
+       - else if Mode == RESIZE
+               The child after us is resized. */
+
+namespace giada::v
+{
+class geResizerBar : public Fl_Box
+{
+public:
+       enum class Direction
+       {
+               HORIZONTAL,
+               VERTICAL
+       };
+
+       enum class Mode
+       {
+               MOVE,
+               RESIZE
+       };
+
+       geResizerBar(int x, int y, int w, int h, int minSize, Direction dir, Mode m = Mode::MOVE);
+
+       int  handle(int e) override;
+       void draw() override;
+       void resize(int x, int y, int w, int h) override;
+
+       void moveTo(int p);
+
+       std::function<void(const Fl_Widget&)> onDrag    = nullptr;
+       std::function<void(const Fl_Widget&)> onRelease = nullptr;
+
+private:
+       /* isBefore
+       True if widget 'w' is before the drag bar. */
+
+       bool isBefore(const Fl_Widget& w) const;
+
+       /* isBefore
+       True if widget 'w' is after the drag bar. */
+       bool isAfter(const Fl_Widget& w) const;
+
+       /* findWidgets
+       Returns a vector of widgets according to a certain logic specified in the
+       lambda function. Limits the output to 'howmany' widgets if 'howmany' != -1. */
+
+       std::vector<Fl_Widget*> findWidgets(std::function<bool(const Fl_Widget&)> f, int howmany = -1) const;
+
+       /* handleDrag
+       Main entrypoint for the dragging operation. */
+
+       void handleDrag(int diff);
+
+       /* move
+       Resize the first widget and shift all others. */
+
+       void move(int diff);
+
+       /* resize 
+       Resize the first and the second widget, leaving all others untouched. */
+
+       void resize(int diff);
+
+       /* getFirstWidget
+       Returns a ref to the first widget before the drag bar. */
+
+       Fl_Widget& getFirstWidget();
+
+       Direction m_direction;
+       Mode      m_mode;
+       int       m_minSize;
+       int       m_lastPos;
+       bool      m_hover;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/scroll.cpp b/src/gui/elems/basics/scroll.cpp
new file mode 100644 (file)
index 0000000..8f4837c
--- /dev/null
@@ -0,0 +1,97 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geScroll
+ * Custom scroll with nice scrollbars and something else.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "scroll.h"
+#include "boxtypes.h"
+#include "core/const.h"
+#include <cassert>
+
+geScroll::geScroll(int x, int y, int w, int h, int t)
+: Fl_Scroll(x, y, w, h)
+{
+       end();
+       type(t);
+
+       scrollbar.color(G_COLOR_GREY_2);
+       scrollbar.selection_color(G_COLOR_GREY_4);
+       scrollbar.labelcolor(G_COLOR_LIGHT_1);
+       scrollbar.slider(G_CUSTOM_BORDER_BOX);
+       scrollbar.callback(cb_onScrollV, this);
+
+       hscrollbar.color(G_COLOR_GREY_2);
+       hscrollbar.selection_color(G_COLOR_GREY_4);
+       hscrollbar.labelcolor(G_COLOR_LIGHT_1);
+       hscrollbar.slider(G_CUSTOM_BORDER_BOX);
+       hscrollbar.callback(cb_onScrollH, this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geScroll::cb_onScrollV(Fl_Widget* w, void* p)
+{
+       geScroll*     s = static_cast<geScroll*>(w->parent());
+       Fl_Scrollbar* b = static_cast<Fl_Scrollbar*>(w);
+
+       s->scroll_to(s->xposition(), b->value());
+
+       (static_cast<geScroll*>(p))->cb_onScrollV();
+}
+
+void geScroll::cb_onScrollH(Fl_Widget* w, void* p)
+{
+       geScroll*     s = static_cast<geScroll*>(w->parent());
+       Fl_Scrollbar* b = static_cast<Fl_Scrollbar*>(w);
+
+       s->scroll_to(b->value(), s->yposition());
+
+       (static_cast<geScroll*>(p))->cb_onScrollH();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geScroll::cb_onScrollV()
+{
+       if (onScrollV != nullptr)
+               onScrollV(yposition());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geScroll::cb_onScrollH()
+{
+       if (onScrollH != nullptr)
+               onScrollH(xposition());
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geScroll::countChildren() const
+{
+       return children() - 2; // Exclude scrollbars
+}
\ No newline at end of file
diff --git a/src/gui/elems/basics/scroll.h b/src/gui/elems/basics/scroll.h
new file mode 100644 (file)
index 0000000..d13a8e8
--- /dev/null
@@ -0,0 +1,53 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geScroll
+ * Custom scroll with nice scrollbars and something else.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SCROLL_H
+#define GE_SCROLL_H
+
+#include <FL/Fl_Scroll.H>
+#include <functional>
+
+class geScroll : public Fl_Scroll
+{
+public:
+       geScroll(int x, int y, int w, int h, int type = Fl_Scroll::BOTH);
+
+       int countChildren() const;
+
+       std::function<void(int)> onScrollV{nullptr};
+       std::function<void(int)> onScrollH{nullptr};
+
+private:
+       static void cb_onScrollV(Fl_Widget* w, void* p);
+       static void cb_onScrollH(Fl_Widget* w, void* p);
+       void        cb_onScrollV();
+       void        cb_onScrollH();
+};
+
+#endif
diff --git a/src/gui/elems/basics/scrollPack.cpp b/src/gui/elems/basics/scrollPack.cpp
new file mode 100644 (file)
index 0000000..0b5892a
--- /dev/null
@@ -0,0 +1,82 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "scrollPack.h"
+#include "boxtypes.h"
+#include "core/const.h"
+#include <cassert>
+#include <cstddef>
+
+namespace giada
+{
+namespace v
+{
+geScrollPack::geScrollPack(int x, int y, int w, int h, int type, Direction dir,
+    int gutter)
+: geScroll(x, y, w, h, type)
+, m_direction(dir)
+, m_gutter(gutter)
+{
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::size_t geScrollPack::countChildren() const
+{
+       return m_widgets.size();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geScrollPack::add(Fl_Widget* w)
+{
+       if (countChildren() == 0)
+               w->position(x(), y());
+       else if (m_direction == Direction::HORIZONTAL)
+               w->position((getLastChild()->x() + getLastChild()->w() + m_gutter), y());
+       else
+               w->position(x(), (getLastChild()->y() + getLastChild()->h() + m_gutter));
+
+       geScroll::add(w);
+       m_widgets.push_back(w);
+}
+
+/* -------------------------------------------------------------------------- */
+
+Fl_Widget* geScrollPack::getChild(std::size_t i)
+{
+       return m_widgets.at(i); // Throws std::out_of_range in case
+}
+
+/* -------------------------------------------------------------------------- */
+
+Fl_Widget* geScrollPack::getLastChild()
+{
+       return m_widgets.at(m_widgets.size() - 1); // Throws std::out_of_range in case
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/basics/scrollPack.h b/src/gui/elems/basics/scrollPack.h
new file mode 100644 (file)
index 0000000..14dfe4d
--- /dev/null
@@ -0,0 +1,70 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SCROLL_PACK_H
+#define GE_SCROLL_PACK_H
+
+#include "gui/elems/basics/pack.h"
+#include "gui/elems/basics/scroll.h"
+#include <cstddef>
+
+namespace giada
+{
+namespace v
+{
+/* geScrollPack
+A scrollable viewport that contains packed widgets. */
+
+class geScrollPack : public geScroll
+{
+public:
+       geScrollPack(int x, int y, int w, int h, int type = Fl_Scroll::BOTH,
+           Direction d = Direction::HORIZONTAL, int gutter = G_GUI_INNER_MARGIN);
+
+       /* countChildren
+    Returns the number of widgets contained in this group. */
+
+       std::size_t countChildren() const;
+
+       void add(Fl_Widget* w);
+
+       Fl_Widget* getChild(std::size_t i);
+       Fl_Widget* getLastChild();
+
+  private:
+       /* m_widgets 
+    The internal Fl_Scroll::array_ is unreliable when inspected with the child()
+    method. Let's keep track of widgets that belong to this group manually. */
+
+       std::vector<Fl_Widget*> m_widgets;
+
+       Direction m_direction;
+       int       m_gutter;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/elems/basics/slider.cpp b/src/gui/elems/basics/slider.cpp
new file mode 100644 (file)
index 0000000..e018452
--- /dev/null
@@ -0,0 +1,43 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "slider.h"
+#include "../../../core/const.h"
+#include "boxtypes.h"
+
+geSlider::geSlider(int x, int y, int w, int h, const char* l)
+: Fl_Slider(x, y, w, h, l)
+{
+       type(FL_HOR_FILL_SLIDER);
+
+       labelsize(G_GUI_FONT_SIZE_BASE);
+       align(FL_ALIGN_LEFT);
+       labelcolor(G_COLOR_LIGHT_2);
+
+       box(G_CUSTOM_BORDER_BOX);
+       color(G_COLOR_GREY_2);
+       selection_color(G_COLOR_GREY_4);
+}
diff --git a/src/gui/elems/basics/slider.h b/src/gui/elems/basics/slider.h
new file mode 100644 (file)
index 0000000..5f47744
--- /dev/null
@@ -0,0 +1,40 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SLIDER_H
+#define GE_SLIDER_H
+
+#include <FL/Fl_Slider.H>
+
+class geSlider : public Fl_Slider
+{
+public:
+       geSlider(int x, int y, int w, int h, const char* l = 0);
+
+       int id;
+};
+
+#endif
diff --git a/src/gui/elems/basics/split.cpp b/src/gui/elems/basics/split.cpp
new file mode 100644 (file)
index 0000000..a296082
--- /dev/null
@@ -0,0 +1,69 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "split.h"
+#include "core/const.h"
+#include "gui/elems/basics/box.h"
+
+namespace giada::v
+{
+geSplit::geSplit(int x, int y, int w, int h)
+: Fl_Group(x, y, w, h)
+, m_a(nullptr)
+, m_b(nullptr)
+, m_bar(0, 0, w, G_GUI_INNER_MARGIN, G_GUI_UNIT, geResizerBar::Direction::VERTICAL, geResizerBar::Mode::RESIZE)
+{
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSplit::init(Fl_Widget& a, Fl_Widget& b)
+{
+       a.resize(x(), y(), w(), (h() / 2) - G_GUI_INNER_MARGIN); // Panel A goes on top
+       a.redraw();
+       m_a = &a;
+
+       m_bar.resize(x(), m_a->y() + m_a->h(), w(), G_GUI_INNER_MARGIN);
+
+       b.resize(x(), m_bar.y() + m_bar.h(), w(), h() / 2); // Panel B goes on bottom
+       b.redraw();
+       m_b = &b;
+
+       Fl_Group::add(m_a);
+       Fl_Group::add(m_bar);
+       Fl_Group::add(m_b);
+
+       resizable(m_a);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSplit::resizePanel(Panel p, int s)
+{
+       m_bar.moveTo(p == Panel::A ? s : h() - s);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/split.h b/src/gui/elems/basics/split.h
new file mode 100644 (file)
index 0000000..237db18
--- /dev/null
@@ -0,0 +1,60 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SPLIT_H
+#define GE_SPLIT_H
+
+#include "gui/elems/basics/resizerBar.h"
+#include <FL/Fl_Group.H>
+
+namespace giada::v
+{
+/* geSplit
+A resizable split-view widget that contains two horizontal panels (A and B). 
+TODO - add vertical mode. */
+
+class geSplit : public Fl_Group
+{
+public:
+       enum class Panel
+       {
+               A,
+               B
+       };
+
+       geSplit(int x, int y, int w, int h);
+
+       void init(Fl_Widget& a, Fl_Widget& b);
+       void resizePanel(Panel p, int s);
+
+private:
+       Fl_Widget*   m_a;
+       Fl_Widget*   m_b;
+       geResizerBar m_bar;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/statusButton.cpp b/src/gui/elems/basics/statusButton.cpp
new file mode 100644 (file)
index 0000000..ee6733e
--- /dev/null
@@ -0,0 +1,68 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geStatusButton
+ * Simple geButton with a boolean 'status' parameter.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "statusButton.h"
+#include "core/const.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geStatusButton::geStatusButton(int x, int y, int w, int h, const char** imgOff,
+    const char** imgOn, const char** imgDisabled)
+: geButton(x, y, w, h, "", imgOff, imgOn, imgDisabled)
+, m_status(false)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geStatusButton::draw()
+{
+       if (active())
+               if (m_status)
+                       geButton::draw(imgOn, bgColor1, txtColor);
+               else
+                       geButton::draw(imgOff, bgColor0, txtColor);
+       else
+               geButton::draw(imgDisabled, bgColor0, bdColor);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geStatusButton::setStatus(bool s)
+{
+       m_status = s;
+       redraw();
+}
+
+bool geStatusButton::getStatus() const
+{
+       return m_status;
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/basics/statusButton.h b/src/gui/elems/basics/statusButton.h
new file mode 100644 (file)
index 0000000..1c9a917
--- /dev/null
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * geStatusButton
+ * Simple geButton with a boolean 'status' parameter.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_STATUS_BUTTON_H
+#define GE_STATUS_BUTTON_H
+
+#include "button.h"
+
+namespace giada::v
+{
+class geStatusButton : public geButton
+{
+  public:
+       geStatusButton(int x, int y, int w, int h, const char** imgOff = nullptr,
+           const char** imgOn = nullptr, const char** imgDisabled = nullptr);
+
+       void draw() override;
+
+       bool getStatus() const;
+
+       void setStatus(bool s);
+
+  private:
+       bool m_status;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/basics/tabs.cpp b/src/gui/elems/basics/tabs.cpp
new file mode 100644 (file)
index 0000000..b2aeca9
--- /dev/null
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/basics/tabs.h"
+#include "core/const.h"
+#include "gui/elems/basics/boxtypes.h"
+
+namespace giada::v
+{
+geTabs::geTabs(geompp::Rect<int> r)
+: Fl_Tabs(r.x, r.y, r.w, r.h)
+{
+       box(G_CUSTOM_BORDER_BOX);
+       labelcolor(G_COLOR_LIGHT_2);
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabs::add(Fl_Widget* wg)
+{
+       constexpr int TAB_HEIGHT = 25;
+
+       wg->resize(x(), y() + TAB_HEIGHT, w(), h() - TAB_HEIGHT);
+       wg->labelsize(G_GUI_FONT_SIZE_BASE);
+       wg->selection_color(G_COLOR_GREY_4);
+
+       Fl_Tabs::add(wg);
+       resizable(wg);//  To keep the tab height constant during resizing
+}
+} // namespace giada::v
diff --git a/src/gui/elems/basics/tabs.h b/src/gui/elems/basics/tabs.h
new file mode 100644 (file)
index 0000000..37229f2
--- /dev/null
@@ -0,0 +1,44 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_TABS_H
+#define GE_TABS_H
+
+#include "deps/geompp/src/rect.hpp"
+#include <FL/Fl_Tabs.H>
+
+namespace giada::v
+{
+class geTabs : public Fl_Tabs
+{
+public:
+       geTabs(geompp::Rect<int>);
+
+       void add(Fl_Widget*);
+};
+} // namespace giada::v
+
+#endif
\ No newline at end of file
diff --git a/src/gui/elems/config/tabAudio.cpp b/src/gui/elems/config/tabAudio.cpp
new file mode 100644 (file)
index 0000000..aaac8de
--- /dev/null
@@ -0,0 +1,330 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "tabAudio.h"
+#include "core/const.h"
+#include "core/kernelAudio.h"
+#include "deps/rtaudio/RtAudio.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/choice.h"
+#include "gui/elems/basics/flex.h"
+#include "gui/elems/basics/input.h"
+#include "utils/string.h"
+#include <string>
+
+constexpr int LABEL_WIDTH = 110;
+
+namespace giada::v
+{
+geTabAudio::geDeviceMenu::geDeviceMenu(const char* l, const std::vector<c::config::AudioDeviceData>& devices)
+: geChoice(l, LABEL_WIDTH)
+{
+       if (devices.size() == 0)
+       {
+               addItem("-- no devices found --", 0);
+               showItem(0);
+               return;
+       }
+
+       for (const c::config::AudioDeviceData& device : devices)
+               addItem(device.name, device.index);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geTabAudio::geChannelMenu::geChannelMenu(const char* l, const c::config::AudioDeviceData& data)
+: geChoice(l, LABEL_WIDTH)
+, m_data(data)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geTabAudio::geChannelMenu::getChannelsCount() const
+{
+       return getSelectedId() < STEREO_OFFSET ? 1 : 2;
+}
+
+int geTabAudio::geChannelMenu::getChannelsStart() const
+{
+       if (m_data.channelsCount == 1)
+               return getSelectedId();
+       return getSelectedId() < STEREO_OFFSET ? getSelectedId() : getSelectedId() - STEREO_OFFSET;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabAudio::geChannelMenu::rebuild(const c::config::AudioDeviceData& data)
+{
+       m_data = data;
+
+       clear();
+
+       if (m_data.index == -1)
+       {
+               addItem("none", 0);
+               showItem(0);
+               return;
+       }
+
+       if (m_data.type == c::config::DeviceType::INPUT)
+               for (int i = 0; i < m_data.channelsMax; i++)
+                       addItem(std::to_string(i + 1), i);
+
+       /* Dirty trick for stereo channels: they start at STEREO_OFFSET. */
+
+       for (int i = 0; i < m_data.channelsMax; i += 2)
+               addItem(std::to_string(i + 1) + "-" + std::to_string(i + 2), i + STEREO_OFFSET);
+
+       if (m_data.channelsCount == 1)
+               showItem(m_data.channelsStart);
+       else
+               showItem(m_data.channelsStart + STEREO_OFFSET);
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geTabAudio::geTabAudio(geompp::Rect<int> bounds)
+: Fl_Group(bounds.x, bounds.y, bounds.w, bounds.h, "Audio")
+, m_data(c::config::getAudioData())
+, m_initialApi(m_data.api)
+{
+       end();
+
+       geFlex* body = new geFlex(bounds.reduced(G_GUI_OUTER_MARGIN), Direction::VERTICAL, G_GUI_OUTER_MARGIN);
+       {
+               soundsys = new geChoice("System", LABEL_WIDTH);
+
+               geFlex* line1 = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       buffersize = new geChoice("Buffer size", LABEL_WIDTH);
+                       samplerate = new geChoice("Sample rate", LABEL_WIDTH);
+
+                       line1->add(buffersize, 180);
+                       line1->add(samplerate, 180);
+                       line1->end();
+               }
+
+               sounddevOut = new geDeviceMenu("Output device", m_data.outputDevices);
+
+               geFlex* line2 = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       channelsOut = new geChannelMenu("Output channels", m_data.outputDevice);
+                       limitOutput = new geCheck(x() + 177, y() + 93, 100, 20, "Limit output");
+
+                       line2->add(channelsOut, 180);
+                       line2->add(limitOutput);
+                       line2->end();
+               }
+
+               geFlex* line3 = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       sounddevIn = new geDeviceMenu("Input device", m_data.inputDevices);
+                       enableIn   = new geCheck(0, 0, 0, 0);
+
+                       line3->add(sounddevIn);
+                       line3->add(enableIn, 12);
+                       line3->end();
+               }
+
+               geFlex* line4 = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       channelsIn      = new geChannelMenu("Input channels", m_data.inputDevice);
+                       recTriggerLevel = new geInput(0, 0, 0, 0, "Rec threshold (dB)");
+
+                       line4->add(channelsIn, 180);
+                       line4->add(new geBox(), 132); // TODO - temporary hack for geInput's label
+                       line4->add(recTriggerLevel, 40);
+                       line4->end();
+               }
+
+               rsmpQuality = new geChoice("Resampling", LABEL_WIDTH);
+
+               body->add(soundsys, 20);
+               body->add(line1, 20);
+               body->add(sounddevOut, 20);
+               body->add(line2, 20);
+               body->add(line3, 20);
+               body->add(line4, 20);
+               body->add(rsmpQuality, 20);
+               body->add(new geBox("Restart Giada for the changes to take effect."));
+               body->end();
+       }
+
+       add(body);
+       resizable(body);
+
+       for (const auto& [key, value] : m_data.apis)
+               soundsys->addItem(value.c_str(), key);
+       soundsys->showItem(m_data.api);
+       soundsys->onChange = [this](ID id) { m_data.api = id; invalidate(); };
+
+       samplerate->onChange = [this](ID id) { m_data.sampleRate = id; };
+
+       sounddevOut->showItem(m_data.outputDevice.index);
+       sounddevOut->onChange = [this](ID id) { m_data.setOutputDevice(id); fetch(); };
+
+       sounddevIn->showItem(m_data.inputDevice.index);
+       sounddevIn->onChange = [this](ID id) { m_data.setInputDevice(id); fetch(); };
+
+       enableIn->copy_tooltip("Enable Input");
+       enableIn->value(m_data.inputDevice.index != -1);
+       enableIn->onChange = [this](bool b) { m_data.setInputDevice(b ? 0 : -1); fetch(); };
+
+       channelsOut->onChange = [this](ID) {
+               m_data.outputDevice.channelsCount = channelsOut->getChannelsCount();
+               m_data.outputDevice.channelsStart = channelsOut->getChannelsStart();
+       };
+
+       channelsIn->onChange = [this](ID) {
+               m_data.inputDevice.channelsCount = channelsIn->getChannelsCount();
+               m_data.inputDevice.channelsStart = channelsIn->getChannelsStart();
+       };
+
+       limitOutput->value(m_data.limitOutput);
+       limitOutput->onChange = [this](bool v) { m_data.limitOutput = v; };
+
+       buffersize->addItem("8", 8);
+       buffersize->addItem("16", 16);
+       buffersize->addItem("32", 32);
+       buffersize->addItem("64", 64);
+       buffersize->addItem("128", 128);
+       buffersize->addItem("256", 256);
+       buffersize->addItem("512", 512);
+       buffersize->addItem("1024", 1024);
+       buffersize->addItem("2048", 2048);
+       buffersize->addItem("4096", 4096);
+       buffersize->showItem(m_data.bufferSize);
+       buffersize->onChange = [this](ID id) { m_data.bufferSize = id; };
+
+       rsmpQuality->addItem("Sinc best quality (very slow)", 0);
+       rsmpQuality->addItem("Sinc medium quality (slow)", 1);
+       rsmpQuality->addItem("Sinc basic quality (medium)", 2);
+       rsmpQuality->addItem("Zero Order Hold (fast)", 3);
+       rsmpQuality->addItem("Linear (very fast)", 4);
+       rsmpQuality->showItem(m_data.resampleQuality);
+       rsmpQuality->onChange = [this](ID id) { m_data.resampleQuality = id; };
+
+       recTriggerLevel->value(u::string::fToString(m_data.recTriggerLevel, 1).c_str());
+       recTriggerLevel->onChange = [this](const std::string& s) { m_data.recTriggerLevel = std::stof(s); };
+
+       if (m_data.api == G_SYS_API_NONE)
+               deactivateAll();
+       else
+               fetch();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabAudio::invalidate()
+{
+       /* If the user changes sound system (e.g. ALSA->JACK), deactivate all widgets. */
+
+       if (m_initialApi == m_data.api && m_initialApi != -1 && m_data.api != G_SYS_API_NONE)
+               activateAll();
+       else
+               deactivateAll();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabAudio::fetch()
+{
+       for (int sampleRate : m_data.outputDevice.sampleRates)
+               samplerate->addItem(std::to_string(sampleRate), sampleRate);
+       samplerate->showItem(m_data.sampleRate);
+
+       channelsOut->rebuild(m_data.outputDevice);
+       m_data.outputDevice.channelsCount = channelsOut->getChannelsCount();
+       m_data.outputDevice.channelsStart = channelsOut->getChannelsStart();
+
+       if (m_data.api == G_SYS_API_JACK)
+               buffersize->deactivate();
+       else
+               buffersize->activate();
+
+       if (m_data.inputDevice.index != -1)
+       {
+               channelsIn->rebuild(m_data.inputDevice);
+               m_data.inputDevice.channelsCount = channelsIn->getChannelsCount();
+               m_data.inputDevice.channelsStart = channelsIn->getChannelsStart();
+               sounddevIn->activate();
+               channelsIn->activate();
+               recTriggerLevel->activate();
+       }
+       else
+       {
+               sounddevIn->deactivate();
+               channelsIn->deactivate();
+               recTriggerLevel->deactivate();
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabAudio::deactivateAll()
+{
+       buffersize->deactivate();
+       limitOutput->deactivate();
+       sounddevOut->deactivate();
+       channelsOut->deactivate();
+       samplerate->deactivate();
+       sounddevIn->deactivate();
+       channelsIn->deactivate();
+       recTriggerLevel->deactivate();
+       rsmpQuality->deactivate();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabAudio::activateAll()
+{
+       buffersize->activate();
+       limitOutput->activate();
+       sounddevOut->activate();
+       channelsOut->activate();
+       samplerate->activate();
+       rsmpQuality->activate();
+       if (m_data.inputDevice.index != -1)
+       {
+               sounddevIn->activate();
+               channelsIn->activate();
+               recTriggerLevel->activate();
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabAudio::save()
+{
+       c::config::save(m_data);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/config/tabAudio.h b/src/gui/elems/config/tabAudio.h
new file mode 100644 (file)
index 0000000..52d3dee
--- /dev/null
@@ -0,0 +1,91 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_TAB_AUDIO_H
+#define GE_TAB_AUDIO_H
+
+#include "deps/geompp/src/rect.hpp"
+#include "glue/config.h"
+#include "gui/elems/basics/choice.h"
+#include <FL/Fl_Group.H>
+
+class geCheck;
+class geInput;
+
+namespace giada::v
+{
+class geTabAudio : public Fl_Group
+{
+public:
+       struct geDeviceMenu : public geChoice
+       {
+               geDeviceMenu(const char* l, const std::vector<c::config::AudioDeviceData>&);
+       };
+
+       struct geChannelMenu : public geChoice
+       {
+               geChannelMenu(const char* l, const c::config::AudioDeviceData&);
+
+               int getChannelsCount() const;
+               int getChannelsStart() const;
+
+               void rebuild(const c::config::AudioDeviceData&);
+
+       private:
+               static constexpr int STEREO_OFFSET = 1000;
+
+               c::config::AudioDeviceData m_data;
+       };
+
+       geTabAudio(geompp::Rect<int>);
+
+       void save();
+
+       geChoice*      soundsys;
+       geChoice*      buffersize;
+       geChoice*      samplerate;
+       geDeviceMenu*  sounddevOut;
+       geChannelMenu* channelsOut;
+       geCheck*       limitOutput;
+       geDeviceMenu*  sounddevIn;
+       geCheck*       enableIn;
+       geChannelMenu* channelsIn;
+       geInput*       recTriggerLevel;
+       geChoice*      rsmpQuality;
+
+private:
+       void invalidate();
+       void fetch();
+       void deactivateAll();
+       void activateAll();
+
+       c::config::AudioData m_data;
+
+       int m_initialApi;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/config/tabBehaviors.cpp b/src/gui/elems/config/tabBehaviors.cpp
new file mode 100644 (file)
index 0000000..bcf2494
--- /dev/null
@@ -0,0 +1,75 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "tabBehaviors.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/flex.h"
+#include <FL/Fl_Pack.H>
+
+namespace giada::v
+{
+geTabBehaviors::geTabBehaviors(geompp::Rect<int> bounds, m::Conf::Data& c)
+: Fl_Group(bounds.x, bounds.y, bounds.w, bounds.h, "Behaviors")
+, m_conf(c)
+{
+       end();
+
+       geFlex* body = new geFlex(bounds.reduced(G_GUI_OUTER_MARGIN), Direction::VERTICAL, G_GUI_OUTER_MARGIN);
+       {
+               m_chansStopOnSeqHalt         = new geCheck(0, 0, 0, 0, "Dynamic channels stop immediately when the sequencer\nis halted");
+               m_treatRecsAsLoops           = new geCheck(0, 0, 0, 0, "Treat one shot channels with actions as loops");
+               m_inputMonitorDefaultOn      = new geCheck(0, 0, 0, 0, "New sample channels have input monitor on by default");
+               m_overdubProtectionDefaultOn = new geCheck(0, 0, 0, 0, "New sample channels have overdub protection on\nby default");
+
+               body->add(m_chansStopOnSeqHalt, 30);
+               body->add(m_treatRecsAsLoops, 20);
+               body->add(m_inputMonitorDefaultOn, 20);
+               body->add(m_overdubProtectionDefaultOn, 30);
+               body->end();
+       };
+
+       add(body);
+       resizable(body);
+
+       m_chansStopOnSeqHalt->value(m_conf.chansStopOnSeqHalt);
+       m_treatRecsAsLoops->value(m_conf.treatRecsAsLoops);
+       m_inputMonitorDefaultOn->value(m_conf.inputMonitorDefaultOn);
+       m_overdubProtectionDefaultOn->value(m_conf.overdubProtectionDefaultOn);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabBehaviors::save()
+{
+       m_conf.chansStopOnSeqHalt         = m_chansStopOnSeqHalt->value();
+       m_conf.treatRecsAsLoops           = m_treatRecsAsLoops->value();
+       m_conf.inputMonitorDefaultOn      = m_inputMonitorDefaultOn->value();
+       m_conf.overdubProtectionDefaultOn = m_overdubProtectionDefaultOn->value();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/config/tabBehaviors.h b/src/gui/elems/config/tabBehaviors.h
new file mode 100644 (file)
index 0000000..4e488a7
--- /dev/null
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_TAB_BEHAVIORS_H
+#define GE_TAB_BEHAVIORS_H
+
+#include "core/conf.h"
+#include "deps/geompp/src/rect.hpp"
+#include "gui/elems/basics/check.h"
+#include <FL/Fl_Group.H>
+
+namespace giada::v
+{
+class geTabBehaviors : public Fl_Group
+{
+public:
+       geTabBehaviors(geompp::Rect<int>, m::Conf::Data&);
+
+       void save();
+
+private:
+       geCheck* m_chansStopOnSeqHalt;
+       geCheck* m_treatRecsAsLoops;
+       geCheck* m_inputMonitorDefaultOn;
+       geCheck* m_overdubProtectionDefaultOn;
+
+       m::Conf::Data& m_conf;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/config/tabBindings.cpp b/src/gui/elems/config/tabBindings.cpp
new file mode 100644 (file)
index 0000000..59c07b5
--- /dev/null
@@ -0,0 +1,59 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/config/tabBindings.h"
+#include "core/const.h"
+#include "gui/elems/basics/liquidScroll.h"
+#include "gui/elems/keyBinder.h"
+#include "utils/gui.h"
+
+namespace giada::v
+{
+geTabBindings::geTabBindings(geompp::Rect<int> bounds, m::Conf::Data& conf)
+: Fl_Group(bounds.x, bounds.y, bounds.w, bounds.h, "Key Bindings")
+{
+       end();
+
+       geFlex* body = new geFlex(bounds.reduced(G_GUI_OUTER_MARGIN), Direction::VERTICAL, G_GUI_INNER_MARGIN);
+       {
+               play          = new geKeyBinder("Play", conf.keyBindings.find(m::Conf::KEY_BIND_PLAY)->second);
+               rewind        = new geKeyBinder("Rewind", conf.keyBindings.find(m::Conf::KEY_BIND_REWIND)->second);
+               recordActions = new geKeyBinder("Record actions", conf.keyBindings.find(m::Conf::KEY_BIND_RECORD_ACTIONS)->second);
+               recordInput   = new geKeyBinder("Record audio", conf.keyBindings.find(m::Conf::KEY_BIND_RECORD_INPUT)->second);
+               exit          = new geKeyBinder("Exit", conf.keyBindings.find(m::Conf::KEY_BIND_EXIT)->second);
+
+               body->add(play, G_GUI_UNIT);
+               body->add(rewind, G_GUI_UNIT);
+               body->add(recordActions, G_GUI_UNIT);
+               body->add(recordInput, G_GUI_UNIT);
+               body->add(exit, G_GUI_UNIT);
+               body->end();
+       }
+
+       add(body);
+       resizable(body);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/config/tabBindings.h b/src/gui/elems/config/tabBindings.h
new file mode 100644 (file)
index 0000000..fc6aafe
--- /dev/null
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_CONFIG_TAB_BINDINGS_H
+#define GE_CONFIG_TAB_BINDINGS_H
+
+#include "core/conf.h"
+#include "deps/geompp/src/rect.hpp"
+#include <FL/Fl_Group.H>
+
+class geCheck;
+class geInput;
+
+namespace giada::v
+{
+class geKeyBinder;
+class geTabBindings : public Fl_Group
+{
+public:
+       geTabBindings(geompp::Rect<int>, m::Conf::Data&);
+
+private:
+       geKeyBinder* play;
+       geKeyBinder* rewind;
+       geKeyBinder* recordActions;
+       geKeyBinder* recordInput;
+       geKeyBinder* exit;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/config/tabMidi.cpp b/src/gui/elems/config/tabMidi.cpp
new file mode 100644 (file)
index 0000000..c34e628
--- /dev/null
@@ -0,0 +1,194 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/config/tabMidi.h"
+#include "core/const.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/check.h"
+#include "utils/gui.h"
+#include <string>
+
+constexpr int LABEL_WIDTH = 120;
+
+namespace giada::v
+{
+geTabMidi::geMenu::geMenu(const char* l, const std::vector<std::string>& data,
+    const std::string& msgIfNotFound)
+: geChoice(l, LABEL_WIDTH)
+{
+       if (data.size() == 0)
+       {
+               addItem(msgIfNotFound.c_str(), 0);
+               showItem(0);
+               deactivate();
+       }
+       else
+       {
+               for (const std::string& d : data)
+                       addItem(u::gui::removeFltkChars(d).c_str(), -1); // -1: auto-increment ID
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geTabMidi::geTabMidi(geompp::Rect<int> bounds)
+: Fl_Group(bounds.x, bounds.y, bounds.w, bounds.h, "MIDI")
+, m_data(c::config::getMidiData())
+, m_initialApi(m_data.api)
+{
+       end();
+
+       geFlex* body = new geFlex(bounds.reduced(G_GUI_OUTER_MARGIN), Direction::VERTICAL, G_GUI_OUTER_MARGIN);
+       {
+               system = new geChoice("System", LABEL_WIDTH);
+
+               geFlex* line1 = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       portOut   = new geMenu("Output port", m_data.outPorts, "-- no ports found --");
+                       enableOut = new geCheck(0, 0, 0, 0);
+
+                       line1->add(portOut);
+                       line1->add(enableOut, 12);
+                       line1->end();
+               }
+
+               geFlex* line2 = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       portIn   = new geMenu("Input port", m_data.inPorts, "-- no ports found --");
+                       enableIn = new geCheck(0, 0, 0, 0);
+
+                       line2->add(portIn);
+                       line2->add(enableIn, 12);
+                       line2->end();
+               }
+
+               midiMap = new geMenu("Output Midi Map", m_data.midiMaps, "(no MIDI maps available)");
+               sync    = new geChoice("Sync", LABEL_WIDTH);
+
+               body->add(system, 20);
+               body->add(line1, 20);
+               body->add(line2, 20);
+               body->add(midiMap, 20);
+               body->add(sync, 20);
+               body->add(new geBox("Restart Giada for the changes to take effect."));
+               body->end();
+       }
+
+       add(body);
+       resizable(body);
+
+       for (const auto& [key, value] : m_data.apis)
+               system->addItem(value.c_str(), key);
+       system->showItem(m_data.api);
+       system->onChange = [this](ID id) { m_data.api = id; invalidate(); };
+
+       portOut->showItem(m_data.outPort);
+       portOut->onChange = [this](ID id) { m_data.outPort = id; };
+       if (m_data.outPort == -1)
+               portOut->deactivate();
+
+       portIn->showItem(m_data.inPort);
+       portIn->onChange = [this](ID id) { m_data.inPort = id; };
+       if (m_data.inPort == -1)
+               portIn->deactivate();
+
+       enableOut->copy_tooltip("Enable Output port");
+       enableOut->value(m_data.outPort != -1);
+       enableOut->onChange = [this](bool b) {
+               if (b)
+               {
+                       m_data.outPort = portOut->getSelectedId();
+                       portOut->activate();
+               }
+               else
+               {
+                       m_data.outPort = -1;
+                       portOut->deactivate();
+               }
+       };
+
+       enableIn->copy_tooltip("Enable Input port");
+       enableIn->value(m_data.inPort != -1);
+       enableIn->onChange = [this](bool b) {
+               if (b)
+               {
+                       m_data.inPort = portIn->getSelectedId();
+                       portIn->activate();
+               }
+               else
+               {
+                       m_data.inPort = -1;
+                       portIn->deactivate();
+               }
+       };
+
+       midiMap->showItem(m_data.midiMap);
+       midiMap->onChange = [this](ID id) { m_data.midiMap = id; };
+
+       for (const auto& [key, value] : m_data.syncModes)
+               sync->addItem(value.c_str(), key);
+       sync->showItem(m_data.syncMode);
+       sync->onChange = [this](ID id) { m_data.syncMode = id; };
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabMidi::invalidate()
+{
+       /* If the user changes MIDI device (eg ALSA->JACK) device menu deactivates. 
+       If it returns to the original system, we re-fill the list by re-using
+       previous data. */
+
+       if (m_initialApi == m_data.api && m_initialApi != -1)
+       {
+               portOut->activate();
+               portIn->activate();
+               enableOut->activate();
+               enableIn->activate();
+               if (m_data.midiMaps.size() > 0)
+                       midiMap->activate();
+               sync->activate();
+       }
+       else
+       {
+               portOut->deactivate();
+               portIn->deactivate();
+               enableOut->deactivate();
+               enableIn->deactivate();
+               midiMap->deactivate();
+               sync->deactivate();
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabMidi::save() const
+{
+       c::config::save(m_data);
+}
+} // namespace giada::v
diff --git a/src/gui/elems/config/tabMidi.h b/src/gui/elems/config/tabMidi.h
new file mode 100644 (file)
index 0000000..3add0b5
--- /dev/null
@@ -0,0 +1,68 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_TAB_MIDI_H
+#define GE_TAB_MIDI_H
+
+#include "deps/geompp/src/rect.hpp"
+#include "glue/config.h"
+#include "gui/elems/basics/choice.h"
+#include <FL/Fl_Group.H>
+
+class geCheck;
+
+namespace giada::v
+{
+class geTabMidi : public Fl_Group
+{
+public:
+       struct geMenu : public geChoice
+       {
+               geMenu(const char* l, const std::vector<std::string>&, const std::string& msgIfNotFound);
+       };
+
+       geTabMidi(geompp::Rect<int>);
+
+       void save() const;
+
+       geChoice* system;
+       geMenu*   portOut;
+       geMenu*   portIn;
+       geCheck*  enableOut;
+       geCheck*  enableIn;
+       geMenu*   midiMap;
+       geChoice* sync;
+
+private:
+       void invalidate();
+
+       c::config::MidiData m_data;
+
+       int m_initialApi;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/config/tabMisc.cpp b/src/gui/elems/config/tabMisc.cpp
new file mode 100644 (file)
index 0000000..8cf108e
--- /dev/null
@@ -0,0 +1,72 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "tabMisc.h"
+#include "core/const.h"
+#include "gui/elems/basics/choice.h"
+
+constexpr int LABEL_WIDTH = 120;
+
+namespace giada::v
+{
+geTabMisc::geTabMisc(geompp::Rect<int> bounds)
+: Fl_Group(bounds.x, bounds.y, bounds.w, bounds.h, "Misc")
+, m_data(c::config::getMiscData())
+{
+       end();
+
+       geFlex* body = new geFlex(bounds.reduced(G_GUI_OUTER_MARGIN), Direction::VERTICAL, G_GUI_OUTER_MARGIN);
+       {
+               m_debugMsg = new geChoice("Debug messages", LABEL_WIDTH);
+               m_tooltips = new geChoice("Tooltips", LABEL_WIDTH);
+
+               body->add(m_debugMsg, 20);
+               body->add(m_tooltips, 20);
+               body->end();
+       }
+
+       add(body);
+       resizable(body);
+
+       m_debugMsg->addItem("Disabled");
+       m_debugMsg->addItem("To standard output");
+       m_debugMsg->addItem("To file");
+       m_debugMsg->showItem(m_data.logMode);
+       m_debugMsg->onChange = [this](ID id) { m_data.logMode = id; };
+
+       m_tooltips->addItem("Disabled");
+       m_tooltips->addItem("Enabled");
+       m_tooltips->showItem(m_data.showTooltips);
+       m_tooltips->onChange = [this](ID id) { m_data.showTooltips = id; };
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabMisc::save()
+{
+       c::config::save(m_data);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/config/tabMisc.h b/src/gui/elems/config/tabMisc.h
new file mode 100644 (file)
index 0000000..f2d3c5a
--- /dev/null
@@ -0,0 +1,52 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_TAB_MISC_H
+#define GE_TAB_MISC_H
+
+#include "deps/geompp/src/rect.hpp"
+#include "glue/config.h"
+#include <FL/Fl_Group.H>
+
+namespace giada::v
+{
+class geChoice;
+class geTabMisc : public Fl_Group
+{
+public:
+       geTabMisc(geompp::Rect<int>);
+
+       void save();
+
+private:
+       c::config::MiscData m_data;
+
+       geChoice* m_debugMsg;
+       geChoice* m_tooltips;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/config/tabPlugins.cpp b/src/gui/elems/config/tabPlugins.cpp
new file mode 100644 (file)
index 0000000..43f5507
--- /dev/null
@@ -0,0 +1,126 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "tabPlugins.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "glue/layout.h"
+#include "glue/plugin.h"
+#include "gui/dialogs/window.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/check.h"
+#include "gui/elems/basics/flex.h"
+#include "gui/elems/basics/input.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <FL/Fl.H>
+#include <functional>
+
+namespace giada::v
+{
+geTabPlugins::geTabPlugins(geompp::Rect<int> bounds)
+: Fl_Group(bounds.x, bounds.y, bounds.w, bounds.h, "Plug-ins")
+{
+       end();
+
+       geFlex* body = new geFlex(bounds.reduced(G_GUI_OUTER_MARGIN), Direction::VERTICAL, G_GUI_OUTER_MARGIN);
+       {
+               geFlex* line1 = new geFlex(Direction::HORIZONTAL, G_GUI_OUTER_MARGIN);
+               {
+                       m_folderPath = new geInput(0, 0, 0, 0);
+                       m_browse     = new geButton("", zoomInOff_xpm, zoomInOn_xpm);
+
+                       line1->add(new geBox(), 80); // TODO - temporary hack for geInput's label
+                       line1->add(m_folderPath);
+                       line1->add(m_browse, 20);
+                       line1->end();
+               }
+
+               m_scanButton = new geButton();
+               m_info       = new geBox();
+
+               body->add(line1, 20);
+               body->add(m_scanButton, 20);
+               body->add(m_info);
+               body->end();
+       }
+
+       add(body);
+       resizable(body);
+
+       m_info->hide();
+
+       m_folderPath->label("Plug-ins folder");
+       m_folderPath->onChange = [this](const std::string& v) {
+               m_data.pluginPath = v;
+       };
+
+       m_browse->onClick = [this]() {
+               c::layout::openBrowserForPlugins(*static_cast<v::gdWindow*>(top_window()));
+       };
+
+       m_scanButton->onClick = [this]() {
+               std::function<void(float)> callback = [this](float progress) {
+                       std::string l = "Scan in progress (" + std::to_string((int)(progress * 100)) + "%). Please wait...";
+                       m_info->label(l.c_str());
+                       Fl::wait();
+               };
+
+               m_info->show();
+               c::config::scanPlugins(m_folderPath->value(), callback);
+               m_info->hide();
+               rebuild();
+       };
+
+       rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabPlugins::rebuild()
+{
+       m_data = c::config::getPluginData();
+
+       const std::string scanLabel = "Scan (" + std::to_string(m_data.numAvailablePlugins) + " found)";
+       m_scanButton->copy_label(scanLabel.c_str());
+
+       m_folderPath->value(m_data.pluginPath.c_str());
+       m_folderPath->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geTabPlugins::save()
+{
+       c::config::save(m_data);
+}
+} // namespace giada::v
+
+#endif // WITH_VST
\ No newline at end of file
diff --git a/src/gui/elems/config/tabPlugins.h b/src/gui/elems/config/tabPlugins.h
new file mode 100644 (file)
index 0000000..2eea680
--- /dev/null
@@ -0,0 +1,63 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_TAB_PLUGINS_H
+#define GE_TAB_PLUGINS_H
+
+#ifdef WITH_VST
+
+#include "deps/geompp/src/rect.hpp"
+#include "glue/config.h"
+#include <FL/Fl_Group.H>
+
+class geInput;
+class geBox;
+
+namespace giada::v
+{
+class geBox;
+class geButton;
+class geTabPlugins : public Fl_Group
+{
+public:
+       geTabPlugins(geompp::Rect<int>);
+
+       void save();
+       void rebuild();
+
+private:
+       c::config::PluginData m_data;
+
+       geButton* m_browse;
+       geInput*  m_folderPath;
+       geButton* m_scanButton;
+       geBox*    m_info;
+};
+} // namespace giada::v
+
+#endif // WITH_VST
+
+#endif
diff --git a/src/gui/elems/fileBrowser.cpp b/src/gui/elems/fileBrowser.cpp
new file mode 100644 (file)
index 0000000..db021e2
--- /dev/null
@@ -0,0 +1,181 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "fileBrowser.h"
+#include "basics/boxtypes.h"
+#include "core/const.h"
+#include "gui/dialogs/browser/browserBase.h"
+#include "utils/fs.h"
+#include "utils/string.h"
+
+namespace giada
+{
+namespace v
+{
+geFileBrowser::geFileBrowser(int x, int y, int w, int h)
+: Fl_File_Browser(x, y, w, h)
+, m_showHiddenFiles(false)
+{
+       box(G_CUSTOM_BORDER_BOX);
+       textsize(G_GUI_FONT_SIZE_BASE);
+       textcolor(G_COLOR_LIGHT_2);
+       selection_color(G_COLOR_GREY_4);
+       color(G_COLOR_GREY_2);
+       type(FL_SELECT_BROWSER);
+
+       this->scrollbar.color(G_COLOR_GREY_2);
+       this->scrollbar.selection_color(G_COLOR_GREY_4);
+       this->scrollbar.labelcolor(G_COLOR_LIGHT_1);
+       this->scrollbar.slider(G_CUSTOM_BORDER_BOX);
+
+       this->hscrollbar.color(G_COLOR_GREY_2);
+       this->hscrollbar.selection_color(G_COLOR_GREY_4);
+       this->hscrollbar.labelcolor(G_COLOR_LIGHT_1);
+       this->hscrollbar.slider(G_CUSTOM_BORDER_BOX);
+
+       take_focus(); // let it have focus on startup
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geFileBrowser::toggleHiddenFiles()
+{
+       m_showHiddenFiles = !m_showHiddenFiles;
+       loadDir(m_currentDir);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geFileBrowser::loadDir(const std::string& dir)
+{
+       m_currentDir = dir;
+       load(m_currentDir.c_str());
+
+       /* Clean up unwanted elements. Hide "../" first, it just screws up things.
+       Also remove hidden files, if requested. */
+
+       for (int i = size(); i >= 0; i--)
+       {
+               if (text(i) == nullptr)
+                       continue;
+               if (strcmp(text(i), "../") == 0 || (!m_showHiddenFiles && strncmp(text(i), ".", 1) == 0))
+                       remove(i);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geFileBrowser::handle(int e)
+{
+       int ret = Fl_File_Browser::handle(e);
+       switch (e)
+       {
+       case FL_FOCUS:
+       case FL_UNFOCUS:
+               ret = 1; // enables receiving Keyboard events
+               break;
+       case FL_KEYDOWN: // keyboard
+               if (Fl::event_key(FL_Down))
+                       select(value() + 1);
+               else if (Fl::event_key(FL_Up))
+                       select(value() - 1);
+               else if (Fl::event_key(FL_Enter))
+                       static_cast<v::gdBrowserBase*>(parent())->fireCallback();
+               ret = 1;
+               break;
+       case FL_PUSH:                   // mouse
+               if (Fl::event_clicks() > 0) // double click
+                       static_cast<v::gdBrowserBase*>(parent())->fireCallback();
+               ret = 1;
+               break;
+       case FL_RELEASE: // mouse
+               /* nasty trick to keep the selection on mouse release */
+               if (value() > 1)
+               {
+                       select(value() - 1);
+                       select(value() + 1);
+               }
+               else
+               {
+                       select(value() + 1);
+                       select(value() - 1);
+               }
+               ret = 1;
+               break;
+       }
+       return ret;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string geFileBrowser::getCurrentDir()
+{
+       return normalize(u::fs::getRealPath(m_currentDir));
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string geFileBrowser::getSelectedItem(bool fullPath)
+{
+       if (!fullPath) // no full path requested? return the selected text
+               return normalize(text(value()));
+       else if (value() == 0) // no rows selected? return current directory
+               return normalize(m_currentDir);
+       else
+       {
+#ifdef G_OS_WINDOWS
+               std::string sep = m_currentDir != "" ? G_SLASH_STR : "";
+#else
+               std::string sep = G_SLASH_STR;
+#endif
+               return normalize(u::fs::getRealPath(m_currentDir + sep + normalize(text(value()))));
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geFileBrowser::preselect(int pos, int line)
+{
+       position(pos);
+       select(line);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string geFileBrowser::normalize(const std::string& s)
+{
+       std::string out = s;
+
+       /* If std::string ends with G_SLASH, remove it. Don't do it if is the root dir, 
+       that is '/' on Unix or '[x]:\' on Windows. */
+
+       //if (out.back() == G_SLASH && out.length() > 1)
+       if (out.back() == G_SLASH && !u::fs::isRootDir(s))
+               out = out.substr(0, out.size() - 1);
+       return out;
+}
+} // namespace v
+} // namespace giada
\ No newline at end of file
diff --git a/src/gui/elems/fileBrowser.h b/src/gui/elems/fileBrowser.h
new file mode 100644 (file)
index 0000000..624a86a
--- /dev/null
@@ -0,0 +1,75 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * ge_browser
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_FILE_BROWSER_H
+#define GE_FILE_BROWSER_H
+
+#include <FL/Fl_File_Browser.H>
+#include <string>
+
+namespace giada
+{
+namespace v
+{
+class geFileBrowser : public Fl_File_Browser
+{
+public:
+       geFileBrowser(int x, int y, int w, int h);
+
+       void toggleHiddenFiles();
+
+       /* init
+       Initializes browser and show 'dir' as initial directory. */
+
+       void loadDir(const std::string& dir);
+
+       /* getSelectedItem
+       Returns the full path or just the displayed name of the i-th selected item.
+       Always with the trailing slash! */
+
+       std::string getSelectedItem(bool fullPath = true);
+
+       std::string getCurrentDir();
+
+       void preselect(int position, int line);
+
+       int handle(int e);
+
+  private:
+       /* normalize
+       Makes sure the std::string never ends with a trailing slash. */
+
+       std::string normalize(const std::string& s);
+
+       std::string m_currentDir;
+       bool        m_showHiddenFiles;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/elems/keyBinder.cpp b/src/gui/elems/keyBinder.cpp
new file mode 100644 (file)
index 0000000..93d2c3f
--- /dev/null
@@ -0,0 +1,69 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/keyBinder.h"
+#include "core/const.h"
+#include "glue/layout.h"
+#include "gui/dialogs/keyGrabber.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/button.h"
+#include "utils/gui.h"
+
+namespace giada::v
+{
+geKeyBinder::geKeyBinder(const std::string& l, int& keyRef)
+: geFlex(Direction::HORIZONTAL, G_GUI_INNER_MARGIN)
+{
+       m_labelBox = new geBox(l.c_str());
+       m_keyBox   = new geBox(u::gui::keyToString(keyRef).c_str());
+       m_bindBtn  = new geButton("Bind");
+       m_clearBtn = new geButton("Clear");
+
+       add(m_labelBox);
+       add(m_keyBox, 100);
+       add(m_bindBtn, 50);
+       add(m_clearBtn, 50);
+       end();
+
+       m_labelBox->box(G_CUSTOM_BORDER_BOX);
+       m_keyBox->box(G_CUSTOM_BORDER_BOX);
+
+       m_bindBtn->onClick = [&keyRef, this]() {
+               c::layout::openKeyGrabberWindow(keyRef, [&keyRef, this](int newKey) {
+                       keyRef = newKey;
+                       m_keyBox->copy_label(u::gui::keyToString(keyRef).c_str());
+                       return true;
+               });
+       };
+
+       m_clearBtn->onClick = [&keyRef, this]() {
+               keyRef = 0;
+               m_keyBox->copy_label(u::gui::keyToString(keyRef).c_str());
+       };
+}
+
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/keyBinder.h b/src/gui/elems/keyBinder.h
new file mode 100644 (file)
index 0000000..d91eb75
--- /dev/null
@@ -0,0 +1,50 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_KEY_BINDER_H
+#define GE_KEY_BINDER_H
+
+#include "gui/elems/basics/flex.h"
+#include <string>
+
+namespace giada::v
+{
+class geBox;
+class geButton;
+class geKeyBinder : public geFlex
+{
+public:
+       geKeyBinder(const std::string& l, int& keyRef);
+
+private:
+       geBox*    m_labelBox;
+       geBox*    m_keyBox;
+       geButton* m_bindBtn;
+       geButton* m_clearBtn;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/channel.cpp b/src/gui/elems/mainWindow/keyboard/channel.cpp
new file mode 100644 (file)
index 0000000..0b62a1d
--- /dev/null
@@ -0,0 +1,222 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "glue/channel.h"
+#include "core/graphics.h"
+#include "glue/events.h"
+#include "glue/layout.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/statusButton.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/mainWindow/keyboard/channelButton.h"
+#include "gui/elems/mainWindow/keyboard/channelStatus.h"
+#include "gui/elems/mainWindow/keyboard/column.h"
+#include "gui/elems/mainWindow/keyboard/midiActivity.h"
+#include "gui/ui.h"
+#include <FL/Fl.H>
+#include <FL/fl_draw.H>
+
+extern giada::v::Ui g_ui;
+
+namespace giada::v
+{
+geChannel::geChannel(int X, int Y, int W, int H, c::channel::Data d)
+: Fl_Group(X, Y, W, H)
+, m_channel(d)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::draw()
+{
+       const int ny = y() + (h() / 2) - (G_GUI_UNIT / 2);
+
+       playButton->resize(playButton->x(), ny, G_GUI_UNIT, G_GUI_UNIT);
+       arm->resize(arm->x(), ny, G_GUI_UNIT, G_GUI_UNIT);
+       mute->resize(mute->x(), ny, G_GUI_UNIT, G_GUI_UNIT);
+       solo->resize(solo->x(), ny, G_GUI_UNIT, G_GUI_UNIT);
+       vol->resize(vol->x(), ny, G_GUI_UNIT, G_GUI_UNIT);
+#ifdef WITH_VST
+       fx->resize(fx->x(), ny, G_GUI_UNIT, G_GUI_UNIT);
+#endif
+
+       fl_rectf(x(), y(), w(), h(), G_COLOR_GREY_1_5);
+
+       Fl_Group::draw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::cb_arm(Fl_Widget* /*w*/, void* p) { ((geChannel*)p)->cb_arm(); }
+void geChannel::cb_mute(Fl_Widget* /*w*/, void* p) { ((geChannel*)p)->cb_mute(); }
+void geChannel::cb_solo(Fl_Widget* /*w*/, void* p) { ((geChannel*)p)->cb_solo(); }
+void geChannel::cb_changeVol(Fl_Widget* /*w*/, void* p) { ((geChannel*)p)->cb_changeVol(); }
+#ifdef WITH_VST
+void geChannel::cb_openFxWindow(Fl_Widget* /*w*/, void* p)
+{
+       ((geChannel*)p)->cb_openFxWindow();
+}
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::refresh()
+{
+       ChannelStatus playStatus = m_channel.getPlayStatus();
+       ChannelStatus recStatus  = m_channel.getRecStatus();
+
+       if (mainButton->visible())
+               mainButton->refresh();
+
+       if (recStatus == ChannelStatus::WAIT || playStatus == ChannelStatus::WAIT)
+               blink();
+
+       playButton->setStatus(playStatus == ChannelStatus::PLAY || playStatus == ChannelStatus::ENDING);
+       midiActivity->redraw();
+       mute->setStatus(m_channel.getMute());
+       solo->setStatus(m_channel.getSolo());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::cb_arm()
+{
+       c::events::toggleArmChannel(m_channel.id, Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::cb_mute()
+{
+       c::events::toggleMuteChannel(m_channel.id, Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::cb_solo()
+{
+       c::events::toggleSoloChannel(m_channel.id, Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::cb_changeVol()
+{
+       c::events::setChannelVolume(m_channel.id, vol->value(), Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+void geChannel::cb_openFxWindow()
+{
+       c::layout::openChannelPluginListWindow(m_channel.id);
+}
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+int geChannel::getColumnId() const
+{
+       return static_cast<geColumn*>(parent())->id;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::blink()
+{
+       if (g_ui.shouldBlink())
+               mainButton->setPlayMode();
+       else
+               mainButton->setDefaultMode();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannel::packWidgets()
+{
+       /* Compute how much space is visible for the main button, then resize it
+       according to that amount. */
+
+       int visible = w();
+       for (int i = 0; i < children(); i++)
+       {
+               if (child(i)->visible() && child(i) != mainButton)
+                       visible -= child(i)->w() + G_GUI_INNER_MARGIN;
+       }
+
+       mainButton->size(visible, mainButton->h());
+
+       /* Reposition everything else */
+
+       for (int i = 1, p = 0; i < children(); i++)
+       {
+               if (!child(i)->visible())
+                       continue;
+               for (int k = i - 1; k >= 0; k--) // Get the first visible item prior to i
+                       if (child(k)->visible())
+                       {
+                               p = k;
+                               break;
+                       }
+               child(i)->position(child(p)->x() + child(p)->w() + G_GUI_INNER_MARGIN, child(i)->y());
+       }
+
+       init_sizes(); // Resets the internal array of widget sizes and positions
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geChannel::handleKey(int e)
+{
+       if (Fl::event_key() != m_channel.key)
+               return false;
+
+       if (e == FL_KEYDOWN && !playButton->value())
+       {                             // Key not already pressed
+               playButton->take_focus(); // Move focus to this playButton
+               playButton->value(1);
+               return true;
+       }
+
+       if (e == FL_KEYUP)
+       {
+               playButton->value(0);
+               return true;
+       }
+
+       return false;
+}
+
+/* -------------------------------------------------------------------------- */
+
+const c::channel::Data& geChannel::getData() const
+{
+       return m_channel;
+}
+} // namespace giada::v
diff --git a/src/gui/elems/mainWindow/keyboard/channel.h b/src/gui/elems/mainWindow/keyboard/channel.h
new file mode 100644 (file)
index 0000000..c457b46
--- /dev/null
@@ -0,0 +1,129 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_CHANNEL_H
+#define GE_CHANNEL_H
+
+#include "core/types.h"
+#include "glue/channel.h"
+#include <FL/Fl_Group.H>
+
+namespace giada::v
+{
+class geDial;
+class geButton;
+class geChannelStatus;
+class geStatusButton;
+class geChannelButton;
+class geMidiActivity;
+class geChannel : public Fl_Group
+{
+public:
+       geChannel(int x, int y, int w, int h, c::channel::Data d);
+
+       void draw() override;
+
+       /* refresh
+       Updates graphics. */
+
+       virtual void refresh();
+
+       /* getColumnId
+       Returns the ID of the column this channel resides in. */
+
+       ID getColumnId() const;
+
+       /* handleKey
+       Performs some UI-related operations when the bound key is pressed. Returns
+       whether the bound key has been pressed or not. */
+
+       bool handleKey(int e);
+
+       /* getData
+       Returns a reference to the internal data. Read-only. */
+
+       const c::channel::Data& getData() const;
+
+       geStatusButton*  playButton;
+       geButton*        arm;
+       geChannelStatus* status;
+       geChannelButton* mainButton;
+       geMidiActivity*  midiActivity;
+       geStatusButton*  mute;
+       geStatusButton*  solo;
+       geDial*          vol;
+#ifdef WITH_VST
+       geStatusButton* fx;
+#endif
+
+protected:
+       /* Define some breakpoints for dynamic resize. BREAK_DELTA: base amount of
+       pixels to shrink sampleButton. */
+
+#ifdef WITH_VST
+       static const int BREAK_READ_ACTIONS = 240;
+       static const int BREAK_MODE_BOX     = 216;
+       static const int BREAK_FX           = 192;
+       static const int BREAK_ARM          = 168;
+#else
+       static const int BREAK_READ_ACTIONS = 216;
+       static const int BREAK_MODE_BOX     = 192;
+       static const int BREAK_ARM          = 168;
+#endif
+
+       static void cb_arm(Fl_Widget* /*w*/, void* p);
+       static void cb_mute(Fl_Widget* /*w*/, void* p);
+       static void cb_solo(Fl_Widget* /*w*/, void* p);
+       static void cb_changeVol(Fl_Widget* /*w*/, void* p);
+#ifdef WITH_VST
+       static void cb_openFxWindow(Fl_Widget* /*w*/, void* p);
+#endif
+       void cb_mute();
+       void cb_arm();
+       void cb_solo();
+       void cb_changeVol();
+#ifdef WITH_VST
+       void cb_openFxWindow();
+#endif
+
+       /* blink
+       Blinks button when channel is in wait/ending status. */
+
+       void blink();
+
+       /* packWidgets
+       Spread widgets across available space. */
+
+       void packWidgets();
+
+       /* m_channel
+       Channel's data. */
+
+       c::channel::Data m_channel;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/channelButton.cpp b/src/gui/elems/mainWindow/keyboard/channelButton.cpp
new file mode 100644 (file)
index 0000000..cb965d7
--- /dev/null
@@ -0,0 +1,137 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "channelButton.h"
+#include "core/channels/channel.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "glue/channel.h"
+#include "src/core/actions/actions.h"
+#include "utils/string.h"
+#include <FL/fl_draw.H>
+
+namespace giada
+{
+namespace v
+{
+geChannelButton::geChannelButton(int x, int y, int w, int h, const c::channel::Data& d)
+: geButton(x, y, w, h)
+, m_channel(d)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelButton::refresh()
+{
+       switch (m_channel.getPlayStatus())
+       {
+       case ChannelStatus::OFF:
+       case ChannelStatus::EMPTY:
+               setDefaultMode();
+               break;
+       case ChannelStatus::PLAY:
+               setPlayMode();
+               break;
+       case ChannelStatus::ENDING:
+               setEndingMode();
+               break;
+       default:
+               break;
+       }
+       switch (m_channel.getRecStatus())
+       {
+       case ChannelStatus::ENDING:
+               setEndingMode();
+               break;
+       default:
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelButton::draw()
+{
+       geButton::draw();
+
+       if (m_channel.key == 0)
+               return;
+
+       /* draw background */
+
+       fl_rectf(x() + 1, y() + 1, 18, h() - 2, bgColor0);
+
+       /* draw m_key */
+
+       fl_color(G_COLOR_LIGHT_2);
+       fl_font(FL_HELVETICA, 11);
+       fl_draw(std::string(1, static_cast<wchar_t>(m_channel.key)).c_str(), x(), y(), 18, h(), FL_ALIGN_CENTER);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelButton::setInputRecordMode()
+{
+       bgColor0 = G_COLOR_RED;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelButton::setActionRecordMode()
+{
+       bgColor0 = G_COLOR_BLUE;
+       txtColor = G_COLOR_LIGHT_2;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelButton::setDefaultMode(const char* l)
+{
+       bgColor0 = G_COLOR_GREY_2;
+       bdColor  = G_COLOR_GREY_4;
+       txtColor = G_COLOR_LIGHT_2;
+       if (l)
+               label(l);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelButton::setPlayMode()
+{
+       bgColor0 = G_COLOR_LIGHT_1;
+       bdColor  = G_COLOR_LIGHT_1;
+       txtColor = G_COLOR_GREY_1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelButton::setEndingMode()
+{
+       bgColor0 = G_COLOR_GREY_4;
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/mainWindow/keyboard/channelButton.h b/src/gui/elems/mainWindow/keyboard/channelButton.h
new file mode 100644 (file)
index 0000000..400ed3c
--- /dev/null
@@ -0,0 +1,64 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_CHANNEL_BUTTON_H
+#define GE_CHANNEL_BUTTON_H
+
+#include "gui/elems/basics/button.h"
+
+namespace giada
+{
+namespace c
+{
+namespace channel
+{
+struct Data;
+}
+} // namespace c
+namespace v
+{
+class geChannelButton : public geButton
+{
+public:
+       geChannelButton(int x, int y, int w, int h, const c::channel::Data& d);
+
+       virtual void refresh();
+
+       void draw() override;
+
+       void setPlayMode();
+       void setEndingMode();
+       void setDefaultMode(const char* l = 0);
+       void setInputRecordMode();
+       void setActionRecordMode();
+
+  protected:
+       const c::channel::Data& m_channel;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/channelMode.cpp b/src/gui/elems/mainWindow/keyboard/channelMode.cpp
new file mode 100644 (file)
index 0000000..a701ee1
--- /dev/null
@@ -0,0 +1,116 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * ge_modeBox
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "channelMode.h"
+#include "core/channels/channel.h"
+#include "core/channels/samplePlayer.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "core/model/model.h"
+#include "glue/channel.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "utils/gui.h"
+#include <FL/fl_draw.H>
+#include <cassert>
+
+namespace giada::v
+{
+geChannelMode::geChannelMode(int x, int y, int w, int h, c::channel::Data& d)
+: Fl_Menu_Button(x, y, w, h)
+, m_channel(d)
+{
+       box(G_CUSTOM_BORDER_BOX);
+       textsize(G_GUI_FONT_SIZE_BASE);
+       textcolor(G_COLOR_LIGHT_2);
+       color(G_COLOR_GREY_2);
+
+       add("Loop . basic", 0, cb_changeMode, (void*)SamplePlayerMode::LOOP_BASIC);
+       add("Loop . once", 0, cb_changeMode, (void*)SamplePlayerMode::LOOP_ONCE);
+       add("Loop . once . bar", 0, cb_changeMode, (void*)SamplePlayerMode::LOOP_ONCE_BAR);
+       add("Loop . repeat", 0, cb_changeMode, (void*)SamplePlayerMode::LOOP_REPEAT);
+       add("Oneshot . basic", 0, cb_changeMode, (void*)SamplePlayerMode::SINGLE_BASIC);
+       add("Oneshot . basic . pause", 0, cb_changeMode, (void*)SamplePlayerMode::SINGLE_BASIC_PAUSE);
+       add("Oneshot . press", 0, cb_changeMode, (void*)SamplePlayerMode::SINGLE_PRESS);
+       add("Oneshot . retrig", 0, cb_changeMode, (void*)SamplePlayerMode::SINGLE_RETRIG);
+       add("Oneshot . endless", 0, cb_changeMode, (void*)SamplePlayerMode::SINGLE_ENDLESS);
+
+       value(static_cast<int>(m_channel.sample->mode));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelMode::draw()
+{
+       fl_rect(x(), y(), w(), h(), G_COLOR_GREY_4); // border
+
+       switch (m_channel.sample->mode)
+       {
+       case SamplePlayerMode::LOOP_BASIC:
+               fl_draw_pixmap(loopBasic_xpm, x() + 1, y() + 1);
+               break;
+       case SamplePlayerMode::LOOP_ONCE:
+               fl_draw_pixmap(loopOnce_xpm, x() + 1, y() + 1);
+               break;
+       case SamplePlayerMode::LOOP_ONCE_BAR:
+               fl_draw_pixmap(loopOnceBar_xpm, x() + 1, y() + 1);
+               break;
+       case SamplePlayerMode::LOOP_REPEAT:
+               fl_draw_pixmap(loopRepeat_xpm, x() + 1, y() + 1);
+               break;
+       case SamplePlayerMode::SINGLE_BASIC:
+               fl_draw_pixmap(oneshotBasic_xpm, x() + 1, y() + 1);
+               break;
+       case SamplePlayerMode::SINGLE_BASIC_PAUSE:
+               fl_draw_pixmap(oneshotBasicPause_xpm, x() + 1, y() + 1);
+               break;
+       case SamplePlayerMode::SINGLE_PRESS:
+               fl_draw_pixmap(oneshotPress_xpm, x() + 1, y() + 1);
+               break;
+       case SamplePlayerMode::SINGLE_RETRIG:
+               fl_draw_pixmap(oneshotRetrig_xpm, x() + 1, y() + 1);
+               break;
+       case SamplePlayerMode::SINGLE_ENDLESS:
+               fl_draw_pixmap(oneshotEndless_xpm, x() + 1, y() + 1);
+               break;
+       default:
+               assert(false);
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelMode::cb_changeMode(Fl_Widget* w, void* p) { ((geChannelMode*)w)->cb_changeMode((intptr_t)p); }
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelMode::cb_changeMode(int mode)
+{
+       c::channel::setSamplePlayerMode(m_channel.id, static_cast<SamplePlayerMode>(mode));
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/mainWindow/keyboard/channelMode.h b/src/gui/elems/mainWindow/keyboard/channelMode.h
new file mode 100644 (file)
index 0000000..003d98f
--- /dev/null
@@ -0,0 +1,56 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * ge_modeBox
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_CHANNEL_MODE_H
+#define GE_CHANNEL_MODE_H
+
+#include <FL/Fl_Menu_Button.H>
+
+namespace giada::c::channel
+{
+struct Data;
+}
+
+namespace giada::v
+{
+class geChannelMode : public Fl_Menu_Button
+{
+public:
+       geChannelMode(int x, int y, int w, int h, c::channel::Data& d);
+
+       void draw() override;
+
+private:
+       static void cb_changeMode(Fl_Widget* /*w*/, void* p);
+       void        cb_changeMode(int mode);
+
+       c::channel::Data& m_channel;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/channelStatus.cpp b/src/gui/elems/mainWindow/keyboard/channelStatus.cpp
new file mode 100644 (file)
index 0000000..53cb7db
--- /dev/null
@@ -0,0 +1,73 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/keyboard/channelStatus.h"
+#include "core/const.h"
+#include "glue/channel.h"
+#include "utils/math.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geChannelStatus::geChannelStatus(int x, int y, int w, int h, c::channel::Data& d)
+: Fl_Box(x, y, w, h)
+, m_channel(d)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geChannelStatus::draw()
+{
+       fl_rect(x(), y(), w(), h(), G_COLOR_GREY_4);                  // reset border
+       fl_rectf(x() + 1, y() + 1, w() - 2, h() - 2, G_COLOR_GREY_2); // reset background
+
+       const ChannelStatus playStatus = m_channel.getPlayStatus();
+       const ChannelStatus recStatus  = m_channel.getRecStatus();
+       const Frame         tracker    = m_channel.sample->getTracker();
+       const Frame         begin      = m_channel.sample->getBegin();
+       const Frame         end        = m_channel.sample->getEnd();
+       const Pixel         pos        = u::math::map(tracker, begin, end, 0, w());
+
+       if (playStatus == ChannelStatus::WAIT ||
+           playStatus == ChannelStatus::ENDING ||
+           recStatus == ChannelStatus::WAIT ||
+           recStatus == ChannelStatus::ENDING)
+       {
+               fl_rect(x(), y(), w(), h(), G_COLOR_LIGHT_1);
+       }
+       else if (playStatus == ChannelStatus::PLAY)
+       {
+               fl_rect(x(), y(), w(), h(), G_COLOR_LIGHT_1);
+               fl_rectf(x() + 1, y() + 1, pos, h() - 2, G_COLOR_LIGHT_1);
+       }
+       else
+       {
+               fl_rectf(x() + 1, y() + 1, w() - 2, h() - 2, G_COLOR_GREY_2); // status empty
+               fl_rectf(x() + 1, y() + 1, pos, h() - 2, G_COLOR_GREY_4);
+       }
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/mainWindow/keyboard/channelStatus.h b/src/gui/elems/mainWindow/keyboard/channelStatus.h
new file mode 100644 (file)
index 0000000..595557b
--- /dev/null
@@ -0,0 +1,52 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * ge_status
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_CHANNEL_STATUS_H
+#define GE_CHANNEL_STATUS_H
+
+#include <FL/Fl_Box.H>
+
+namespace giada::c::channel
+{
+struct Data;
+}
+namespace giada::v
+{
+class geChannelStatus : public Fl_Box
+{
+public:
+       geChannelStatus(int x, int y, int w, int h, c::channel::Data& d);
+
+       void draw() override;
+
+private:
+       c::channel::Data& m_channel;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/column.cpp b/src/gui/elems/mainWindow/keyboard/column.cpp
new file mode 100644 (file)
index 0000000..237ac61
--- /dev/null
@@ -0,0 +1,193 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "column.h"
+#include "core/model/model.h"
+#include "glue/channel.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/resizerBar.h"
+#include "keyboard.h"
+#include "midiChannel.h"
+#include "sampleChannel.h"
+#include "utils/fs.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include <FL/Fl_Menu_Button.H>
+#include <FL/fl_draw.H>
+#include <cassert>
+
+namespace giada::v
+{
+geColumn::geColumn(int X, int Y, int W, int H, ID id, geResizerBar* b)
+: Fl_Group(X, Y, W, H)
+, id(id)
+, resizerBar(b)
+{
+       end();
+       init();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geColumn::refresh()
+{
+       for (geChannel* c : m_channels)
+               c->refresh();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geColumn::cb_addChannel(Fl_Widget* /*w*/, void* p) { ((geColumn*)p)->cb_addChannel(); }
+
+/* -------------------------------------------------------------------------- */
+
+geChannel* geColumn::addChannel(c::channel::Data d)
+{
+       geChannel* gch  = nullptr;
+       Fl_Widget* last = m_channels.size() == 0 ? static_cast<Fl_Widget*>(m_addChannelBtn) : m_channels.back();
+
+       if (d.type == ChannelType::SAMPLE)
+               gch = new geSampleChannel(x(), last->y() + last->h() + G_GUI_INNER_MARGIN, w(), d.height, d);
+       else
+               gch = new geMidiChannel(x(), last->y() + last->h() + G_GUI_INNER_MARGIN, w(), d.height, d);
+
+       geResizerBar* bar = new geResizerBar(x(), gch->y() + gch->h(), w(),
+           G_GUI_INNER_MARGIN, G_GUI_UNIT, geResizerBar::Direction::VERTICAL,
+           geResizerBar::Mode::MOVE);
+
+       /* Update the column height while dragging the resizer bar. */
+
+       bar->onDrag = [this](const Fl_Widget& /*w*/) {
+               resizable(nullptr);
+               size(this->w(), (child(children() - 1)->y() - y()) + G_GUI_INNER_MARGIN);
+       };
+
+       /* Store the channel height in model when the resizer bar is released. */
+
+       bar->onRelease = [channelId = d.id, this](const Fl_Widget& w) {
+               resizable(this);
+               c::channel::setHeight(channelId, w.h());
+       };
+
+       m_channels.push_back(gch);
+
+       /* Temporarily disable the resizability, add new stuff, resize the group and 
+       bring the resizability back. This is needed to prevent weird vertical 
+       stretching on existing content. */
+
+       resizable(nullptr);
+       add(gch);
+       add(bar);
+       size(w(), computeHeight());
+       init_sizes();
+       resizable(this);
+
+       return gch;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geColumn::cb_addChannel()
+{
+       u::log::print("[geColumn::cb_addChannel] id = %d\n", id);
+
+       Fl_Menu_Item menu[] = {
+           u::gui::makeMenuItem("Add Sample channel"),
+           u::gui::makeMenuItem("Add MIDI channel"),
+           u::gui::makeMenuItem("Remove"),
+           {}};
+
+       if (countChannels() > 0)
+               menu[2].deactivate();
+
+       Fl_Menu_Button b(0, 0, 100, 50);
+       b.box(G_CUSTOM_BORDER_BOX);
+       b.textsize(G_GUI_FONT_SIZE_BASE);
+       b.textcolor(G_COLOR_LIGHT_2);
+       b.color(G_COLOR_GREY_2);
+
+       const Fl_Menu_Item* m = menu->popup(Fl::event_x(), Fl::event_y(), 0, 0, &b);
+       if (m == nullptr)
+               return;
+
+       if (strcmp(m->label(), "Add Sample channel") == 0)
+               c::channel::addChannel(id, ChannelType::SAMPLE);
+       else if (strcmp(m->label(), "Add MIDI channel") == 0)
+               c::channel::addChannel(id, ChannelType::MIDI);
+       else
+               static_cast<geKeyboard*>(parent())->deleteColumn(id);
+}
+
+/* -------------------------------------------------------------------------- */
+
+geChannel* geColumn::getChannel(ID channelId) const
+{
+       for (geChannel* c : m_channels)
+               if (c->getData().id == channelId)
+                       return c;
+       return nullptr;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geColumn::init()
+{
+       Fl_Group::clear();
+       m_channels.clear();
+
+       m_addChannelBtn = new geButton(x(), y(), w(), G_GUI_UNIT, "Edit column");
+       m_addChannelBtn->callback(cb_addChannel, (void*)this);
+
+       add(m_addChannelBtn);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geColumn::forEachChannel(std::function<void(geChannel& c)> f) const
+{
+       for (geChannel* c : m_channels)
+               f(*c);
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geColumn::countChannels() const
+{
+       return m_channels.size();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geColumn::computeHeight() const
+{
+       int out = 0;
+       for (const geChannel* c : m_channels)
+               out += c->h() + G_GUI_INNER_MARGIN;
+       return out + m_addChannelBtn->h() + G_GUI_INNER_MARGIN;
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/mainWindow/keyboard/column.h b/src/gui/elems/mainWindow/keyboard/column.h
new file mode 100644 (file)
index 0000000..808650c
--- /dev/null
@@ -0,0 +1,80 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_COLUMN_H
+#define GE_COLUMN_H
+
+#include "core/types.h"
+#include "glue/channel.h"
+#include <FL/Fl_Group.H>
+#include <functional>
+#include <vector>
+
+namespace giada::v
+{
+class geButton;
+class geResizerBar;
+class geKeyboard;
+class geChannel;
+class geColumn : public Fl_Group
+{
+public:
+       geColumn(int x, int y, int w, int h, ID id, geResizerBar* b);
+
+       geChannel* getChannel(ID channelId) const;
+
+       /* addChannel
+       Adds a new channel in this column. */
+
+       geChannel* addChannel(c::channel::Data d);
+
+       /* refreshChannels
+       Updates channels' graphical statues. Called on each GUI cycle. */
+
+       void refresh();
+
+       void init();
+
+       void forEachChannel(std::function<void(geChannel& c)> f) const;
+
+       ID id;
+
+       geResizerBar* resizerBar;
+
+private:
+       static void cb_addChannel(Fl_Widget* /*w*/, void* p);
+       void        cb_addChannel();
+
+       int countChannels() const;
+       int computeHeight() const;
+
+       std::vector<geChannel*> m_channels;
+
+       geButton* m_addChannelBtn;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/keyboard.cpp b/src/gui/elems/mainWindow/keyboard/keyboard.cpp
new file mode 100644 (file)
index 0000000..4c72ca3
--- /dev/null
@@ -0,0 +1,331 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "glue/channel.h"
+#include "glue/io.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/dispatcher.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/resizerBar.h"
+#include "gui/elems/mainWindow/keyboard/channelButton.h"
+#include "gui/elems/mainWindow/keyboard/column.h"
+#include "gui/elems/mainWindow/keyboard/midiActivity.h"
+#include "gui/elems/mainWindow/keyboard/sampleChannel.h"
+#include "gui/ui.h"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include "utils/string.h"
+#include "utils/vector.h"
+#include <FL/fl_draw.H>
+#include <cassert>
+
+extern giada::v::Ui g_ui;
+
+namespace giada::v
+{
+geKeyboard::geKeyboard(int X, int Y, int W, int H)
+: geScroll(X, Y, W, H, Fl_Scroll::BOTH_ALWAYS)
+, m_addColumnBtn(nullptr)
+{
+       end();
+       init();
+       rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+ID geKeyboard::getChannelColumnId(ID channelId) const
+{
+       return getChannel(channelId)->getColumnId();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::init()
+{
+       m_columnId = m::IdManager();
+
+       deleteAllColumns();
+
+       /* Add 6 empty columns as initial layout. */
+
+       layout.clear();
+       layout.push_back({1, G_DEFAULT_COLUMN_WIDTH});
+       layout.push_back({2, G_DEFAULT_COLUMN_WIDTH});
+       layout.push_back({3, G_DEFAULT_COLUMN_WIDTH});
+       layout.push_back({4, G_DEFAULT_COLUMN_WIDTH});
+       layout.push_back({5, G_DEFAULT_COLUMN_WIDTH});
+       layout.push_back({6, G_DEFAULT_COLUMN_WIDTH});
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::rebuild()
+{
+       /* Wipe out all columns and add them according to the current layout. */
+
+       deleteAllColumns();
+
+       for (ColumnLayout c : layout)
+               addColumn(c.width, c.id);
+
+       for (const c::channel::Data& ch : c::channel::getChannels())
+               getColumn(ch.columnId)->addChannel(ch);
+
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::deleteColumn(ID id)
+{
+       u::vector::removeIf(layout, [=](const ColumnLayout& c) { return c.id == id; });
+       rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::deleteAllColumns()
+{
+       Fl_Scroll::clear();
+       m_columns.clear();
+
+       m_addColumnBtn = new geButton(8, y(), 200, 20, "Add new column");
+       m_addColumnBtn->callback(cb_addColumn, (void*)this);
+       add(m_addColumnBtn);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::setChannelVolume(ID channelId, float v)
+{
+       getChannel(channelId)->vol->value(v);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::notifyMidiIn(ID channelId)
+{
+       getChannel(channelId)->midiActivity->in->lit();
+}
+
+void geKeyboard::notifyMidiOut(ID channelId)
+{
+       getChannel(channelId)->midiActivity->out->lit();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::cb_addColumn(Fl_Widget* /*w*/, void* p)
+{
+       ((geKeyboard*)p)->cb_addColumn();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::refresh()
+{
+       for (geColumn* c : m_columns)
+               c->refresh();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geKeyboard::handle(int e)
+{
+       switch (e)
+       {
+       case FL_FOCUS:
+       case FL_UNFOCUS:
+       {
+               return 1; // Enables receiving Keyboard events
+       }
+       case FL_SHORTCUT: // In case widget that isn't ours has focus
+       case FL_KEYDOWN:  // Keyboard key pushed
+       case FL_KEYUP:
+       { // Keyboard key released
+               g_ui.dispatcher.dispatchKey(e);
+               return 1;
+       }
+       case FL_DND_ENTER: // return(1) for these events to 'accept' dnd
+       case FL_DND_DRAG:
+       case FL_DND_RELEASE:
+       {
+               return 1;
+       }
+       case FL_PASTE:
+       { // handle actual drop (paste) operation
+               const geColumn* c = getColumnAtCursor(Fl::event_x());
+               if (c != nullptr)
+                       c::channel::addAndLoadChannels(c->id, getDroppedFilePaths());
+               return 1;
+       }
+       }
+       return Fl_Group::handle(e); // Assume the buttons won't handle the Keyboard events
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::draw()
+{
+       Fl_Scroll::draw();
+
+       /* Paint columns background. Use a clip to draw only what's visible. */
+
+       fl_color(G_COLOR_GREY_1_5);
+
+       fl_push_clip(
+           x(),
+           y(),
+           w() - scrollbar_size() - (G_GUI_OUTER_MARGIN * 2),
+           h() - scrollbar_size() - (G_GUI_OUTER_MARGIN * 2));
+
+       for (const geColumn* c : m_columns)
+               fl_rectf(c->x(), c->y() + c->h(), c->w(), h() + yposition());
+
+       fl_pop_clip();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::cb_addColumn()
+{
+       addColumn();
+       storeLayout();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::addColumn(int width, ID id)
+{
+       int colx = x() - xposition(); // Mind the x-scroll offset with xposition()
+
+       /* If this is not the first column... */
+
+       if (m_columns.size() > 0)
+               colx = m_columns.back()->x() + m_columns.back()->w() + COLUMN_GAP;
+
+       /* Generate new index. If not passed in. */
+
+       m_columnId.set(id);
+
+       /* Add a new column + a new resizer bar. */
+
+       geResizerBar* bar    = new geResizerBar(colx + width, y(), COLUMN_GAP, h(), G_MIN_COLUMN_WIDTH, geResizerBar::Direction::HORIZONTAL);
+       geColumn*     column = new geColumn(colx, y(), width, G_GUI_UNIT, m_columnId.generate(id), bar);
+
+       /* Store the column width in layout when the resizer bar is released. */
+
+       bar->onRelease = [=](const Fl_Widget& /*w*/) {
+               storeLayout();
+       };
+
+       add(column);
+       add(bar);
+       m_columns.push_back(column);
+
+       /* And then shift the "add column" button on the rightmost edge. */
+
+       m_addColumnBtn->position(colx + width + COLUMN_GAP, y());
+
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::forEachChannel(std::function<void(geChannel& c)> f) const
+{
+       for (geColumn* column : m_columns)
+               column->forEachChannel(f);
+}
+
+void geKeyboard::forEachColumn(std::function<void(const geColumn& c)> f) const
+{
+       for (geColumn* column : m_columns)
+               f(*column);
+}
+
+/* -------------------------------------------------------------------------- */
+
+geColumn* geKeyboard::getColumn(ID id)
+{
+       for (geColumn* c : m_columns)
+               if (c->id == id)
+                       return c;
+       assert(false);
+       return nullptr;
+}
+
+geColumn* geKeyboard::getColumnAtCursor(Pixel px)
+{
+       px += xposition();
+       for (geColumn* c : m_columns)
+               if (px > c->x() && px <= c->x() + c->w())
+                       return c;
+       return nullptr;
+}
+
+/* -------------------------------------------------------------------------- */
+
+const geChannel* geKeyboard::getChannel(ID channelId) const
+{
+       for (geColumn* column : m_columns)
+       {
+               geChannel* c = column->getChannel(channelId);
+               if (c != nullptr)
+                       return c;
+       }
+       assert(false);
+       return nullptr;
+}
+
+geChannel* geKeyboard::getChannel(ID channelId)
+{
+       return const_cast<geChannel*>(const_cast<const geKeyboard*>(this)->getChannel(channelId));
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<std::string> geKeyboard::getDroppedFilePaths() const
+{
+       std::vector<std::string> paths = u::string::split(Fl::event_text(), "\n");
+       for (std::string& p : paths)
+               p = u::fs::stripFileUrl(p);
+       return paths;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geKeyboard::storeLayout()
+{
+       layout.clear();
+       for (const geColumn* c : m_columns)
+               layout.push_back({c->id, c->w()});
+}
+
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/mainWindow/keyboard/keyboard.h b/src/gui/elems/mainWindow/keyboard/keyboard.h
new file mode 100644 (file)
index 0000000..c5593f8
--- /dev/null
@@ -0,0 +1,140 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_KEYBOARD_H
+#define GE_KEYBOARD_H
+
+#include "core/const.h"
+#include "core/idManager.h"
+#include "gui/elems/basics/scroll.h"
+#include <functional>
+#include <vector>
+
+namespace giada::v
+{
+class geButton;
+class geResizerBar;
+class geColumn;
+class geChannel;
+class geKeyboard : public geScroll
+{
+public:
+       struct ColumnLayout
+       {
+               ID  id;
+               int width;
+       };
+
+       geKeyboard(int X, int Y, int W, int H);
+
+       int  handle(int e) override;
+       void draw() override;
+
+       /* getChannelColumnId
+       Given a channel ID, returns the ID of the column it belongs to. */
+
+       ID getChannelColumnId(ID channelId) const;
+
+       /* rebuild
+       Rebuilds this widget from scratch. Used when the model has changed. */
+
+       void rebuild();
+
+       /* refresh
+       Refreshes each column's channel, called on each GUI cycle. */
+
+       void refresh();
+
+       /* deleteColumn
+       Deletes column by id. */
+
+       void deleteColumn(ID id);
+
+       /* deleteAllColumns
+       Deletes all columns from the stack. */
+
+       void deleteAllColumns();
+
+       void setChannelVolume(ID channelId, float v);
+       void notifyMidiIn(ID channelId);
+       void notifyMidiOut(ID channelId);
+
+       /* init
+       Builds the default setup of empty columns. */
+
+       void init();
+
+       void forEachChannel(std::function<void(geChannel& c)> f) const;
+       void forEachColumn(std::function<void(const geColumn& c)> f) const;
+
+       /* layout
+       The column layout. Each element is a column with a specific width. */
+
+       std::vector<ColumnLayout> layout;
+
+private:
+       static constexpr int COLUMN_GAP = 20;
+
+       static void cb_addColumn(Fl_Widget* /*w*/, void* p);
+       void        cb_addColumn();
+
+       void addColumn(int width = G_DEFAULT_COLUMN_WIDTH, ID id = 0);
+
+       /* getDroppedFilePaths
+       Returns a vector of audio file paths after a drag-n-drop from desktop
+       event. */
+
+       std::vector<std::string> getDroppedFilePaths() const;
+
+       /* getColumn
+       Returns the column given the ID. */
+
+       geColumn* getColumn(ID id);
+
+       /* getColumnAtCursor
+       Returns the column below the cursor. */
+
+       geColumn* getColumnAtCursor(Pixel x);
+
+       /* getChannel
+       Given a channel ID returns the UI channel it belongs to. */
+
+       geChannel*       getChannel(ID channelId);
+       const geChannel* getChannel(ID channelId) const;
+
+       /* storeLayout
+       Stores the current column layout into the layout vector. */
+
+       void storeLayout();
+
+       m::IdManager           m_columnId;
+       std::vector<geColumn*> m_columns;
+
+       geButton* m_addColumnBtn;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/midiActivity.cpp b/src/gui/elems/mainWindow/keyboard/midiActivity.cpp
new file mode 100644 (file)
index 0000000..4ced13c
--- /dev/null
@@ -0,0 +1,91 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/keyboard/midiActivity.h"
+#include "core/const.h"
+#include "gui/elems/basics/flex.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geMidiActivity::geLed::geLed()
+: Fl_Button(0, 0, 0, 0)
+, m_decay(0) // decay > 0: led is on
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiActivity::geLed::draw()
+{
+       int bgColor = G_COLOR_GREY_2;
+       int bdColor = G_COLOR_GREY_4;
+
+       if (m_decay > 0) // If led is on
+       {
+               m_decay = (m_decay + 1) % (G_GUI_FPS / 4);
+
+               bgColor = G_COLOR_LIGHT_2;
+               bdColor = G_COLOR_LIGHT_2;
+       }
+
+       fl_rectf(x(), y(), w(), h(), bgColor); // background
+       fl_rect(x(), y(), w(), h(), bdColor);  // border
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiActivity::geLed::lit()
+{
+       m_decay = 1;
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geMidiActivity::geMidiActivity(int x, int y, int w, int h)
+: Fl_Group(x, y, w, h)
+{
+       end();
+
+       geFlex* container = new geFlex(x, y, w, h, Direction::VERTICAL, G_GUI_INNER_MARGIN);
+       {
+               out = new geLed();
+               in  = new geLed();
+
+               container->add(out);
+               container->add(in);
+               container->end();
+       }
+
+       add(container);
+       resizable(container);
+
+       copy_tooltip("MIDI I/O activity\n\nNotifies MIDI messages sent (top) or "
+                    "received (bottom) by this channel.");
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/mainWindow/keyboard/midiActivity.h b/src/gui/elems/mainWindow/keyboard/midiActivity.h
new file mode 100644 (file)
index 0000000..f402a2e
--- /dev/null
@@ -0,0 +1,57 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_MIDI_ACTIVITY_H
+#define GE_MIDI_ACTIVITY_H
+
+#include <FL/Fl_Button.H>
+#include <FL/Fl_Group.H>
+
+namespace giada::v
+{
+class geMidiActivity : public Fl_Group
+{
+public:
+       class geLed : public Fl_Button
+       {
+       public:
+               geLed();
+
+               void draw() override;
+               void lit();
+
+       private:
+               int m_decay;
+       };
+
+       geMidiActivity(int x, int y, int w, int h);
+
+       geLed* out;
+       geLed* in;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/midiChannel.cpp b/src/gui/elems/mainWindow/keyboard/midiChannel.cpp
new file mode 100644 (file)
index 0000000..3273c85
--- /dev/null
@@ -0,0 +1,235 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) G_GUI_UNIT10-G_GUI_UNIT17 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/keyboard/midiChannel.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "glue/channel.h"
+#include "glue/io.h"
+#include "glue/layout.h"
+#include "glue/recorder.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/dispatcher.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/statusButton.h"
+#include "gui/elems/mainWindow/keyboard/column.h"
+#include "gui/elems/mainWindow/keyboard/midiActivity.h"
+#include "gui/elems/mainWindow/keyboard/midiChannelButton.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <FL/Fl_Menu_Button.H>
+#include <cassert>
+
+namespace giada::v
+{
+namespace
+{
+enum class Menu
+{
+       EDIT_ACTIONS = 0,
+       CLEAR_ACTIONS,
+       CLEAR_ACTIONS_ALL,
+       __END_CLEAR_ACTION_SUBMENU__,
+       SETUP_KEYBOARD_INPUT,
+       SETUP_MIDI_INPUT,
+       SETUP_MIDI_OUTPUT,
+       RENAME_CHANNEL,
+       CLONE_CHANNEL,
+       DELETE_CHANNEL
+};
+
+/* -------------------------------------------------------------------------- */
+
+void menuCallback(Fl_Widget* w, void* v)
+{
+       const geMidiChannel*    gch  = static_cast<geMidiChannel*>(w);
+       const c::channel::Data& data = gch->getData();
+
+       switch ((Menu)(intptr_t)v)
+       {
+       case Menu::CLEAR_ACTIONS:
+       case Menu::__END_CLEAR_ACTION_SUBMENU__:
+               break;
+       case Menu::EDIT_ACTIONS:
+               c::layout::openMidiActionEditor(data.id);
+               break;
+       case Menu::CLEAR_ACTIONS_ALL:
+               c::recorder::clearAllActions(data.id);
+               break;
+       case Menu::SETUP_KEYBOARD_INPUT:
+               c::layout::openKeyGrabberWindow(data.key, [channelId = data.id](int key) {
+                       return c::io::channel_setKey(channelId, key);
+               });
+               break;
+       case Menu::SETUP_MIDI_INPUT:
+               c::layout::openChannelMidiInputWindow(data.id);
+               break;
+       case Menu::SETUP_MIDI_OUTPUT:
+               c::layout::openMidiChannelMidiOutputWindow(data.id);
+               break;
+       case Menu::CLONE_CHANNEL:
+               c::channel::cloneChannel(data.id);
+               break;
+       case Menu::RENAME_CHANNEL:
+               c::layout::openRenameChannelWindow(data);
+               break;
+       case Menu::DELETE_CHANNEL:
+               c::channel::deleteChannel(data.id);
+               break;
+       }
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+
+geMidiChannel::geMidiChannel(int X, int Y, int W, int H, c::channel::Data d)
+: geChannel(X, Y, W, H, d)
+, m_data(d)
+{
+       playButton   = new geStatusButton(x(), y(), G_GUI_UNIT, G_GUI_UNIT, channelStop_xpm, channelPlay_xpm);
+       arm          = new geButton(playButton->x() + playButton->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, "", armOff_xpm, armOn_xpm);
+       mainButton   = new geMidiChannelButton(arm->x() + arm->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, H, m_channel);
+       midiActivity = new geMidiActivity(mainButton->x() + mainButton->w() + G_GUI_INNER_MARGIN, y(), 10, h());
+       mute         = new geStatusButton(midiActivity->x() + midiActivity->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, muteOff_xpm, muteOn_xpm);
+       solo         = new geStatusButton(mute->x() + mute->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, soloOff_xpm, soloOn_xpm);
+#if defined(WITH_VST)
+       fx  = new geStatusButton(solo->x() + solo->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, fxOff_xpm, fxOn_xpm);
+       vol = new geDial(fx->x() + fx->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT);
+#else
+       vol = new geDial(solo->x() + solo->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT);
+#endif
+
+       end();
+
+       resizable(mainButton);
+
+       playButton->copy_tooltip("Play/stop");
+       arm->copy_tooltip("Arm for recording");
+       mute->copy_tooltip("Mute");
+       solo->copy_tooltip("Solo");
+#if defined(WITH_VST)
+       fx->copy_tooltip("Plug-ins");
+#endif
+       vol->copy_tooltip("Volume");
+
+#ifdef WITH_VST
+       fx->setStatus(m_channel.plugins.size() > 0);
+#endif
+
+       playButton->callback(cb_playButton, (void*)this);
+       playButton->when(FL_WHEN_CHANGED); // On keypress && on keyrelease
+
+       arm->type(FL_TOGGLE_BUTTON);
+       arm->value(m_channel.isArmed());
+       arm->callback(cb_arm, (void*)this);
+
+#ifdef WITH_VST
+       fx->callback(cb_openFxWindow, (void*)this);
+#endif
+
+       mute->type(FL_TOGGLE_BUTTON);
+       mute->callback(cb_mute, (void*)this);
+
+       solo->type(FL_TOGGLE_BUTTON);
+       solo->callback(cb_solo, (void*)this);
+
+       mainButton->callback(cb_openMenu, (void*)this);
+
+       vol->value(m_channel.volume);
+       vol->callback(cb_changeVol, (void*)this);
+
+       size(w(), h()); // Force responsiveness
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiChannel::cb_playButton(Fl_Widget* /*w*/, void* p) { ((geMidiChannel*)p)->cb_playButton(); }
+void geMidiChannel::cb_openMenu(Fl_Widget* /*w*/, void* p) { ((geMidiChannel*)p)->cb_openMenu(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiChannel::cb_playButton()
+{
+       m_channel.viewDispatcher.dispatchTouch(*this, playButton->value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiChannel::cb_openMenu()
+{
+       Fl_Menu_Item rclick_menu[] = {
+           u::gui::makeMenuItem("Edit actions...", menuCallback, (void*)Menu::EDIT_ACTIONS),
+           u::gui::makeMenuItem("Clear actions", menuCallback, (void*)Menu::CLEAR_ACTIONS, FL_SUBMENU),
+           u::gui::makeMenuItem("All", menuCallback, (void*)Menu::CLEAR_ACTIONS_ALL),
+           {},
+           u::gui::makeMenuItem("Setup keyboard input...", menuCallback, (void*)Menu::SETUP_KEYBOARD_INPUT),
+           u::gui::makeMenuItem("Setup MIDI input...", menuCallback, (void*)Menu::SETUP_MIDI_INPUT),
+           u::gui::makeMenuItem("Setup MIDI output...", menuCallback, (void*)Menu::SETUP_MIDI_OUTPUT),
+           u::gui::makeMenuItem("Rename", menuCallback, (void*)Menu::RENAME_CHANNEL),
+           u::gui::makeMenuItem("Clone", menuCallback, (void*)Menu::CLONE_CHANNEL),
+           u::gui::makeMenuItem("Delete", menuCallback, (void*)Menu::DELETE_CHANNEL),
+           {}};
+
+       /* No 'clear actions' if there are no actions. */
+
+       if (!m_data.hasActions)
+               rclick_menu[(int)Menu::CLEAR_ACTIONS].deactivate();
+
+       Fl_Menu_Button b(0, 0, 100, 50);
+       b.box(G_CUSTOM_BORDER_BOX);
+       b.textsize(G_GUI_FONT_SIZE_BASE);
+       b.textcolor(G_COLOR_LIGHT_2);
+       b.color(G_COLOR_GREY_2);
+
+       const Fl_Menu_Item* m = rclick_menu->popup(Fl::event_x(), Fl::event_y(), 0, 0, &b);
+       if (m != nullptr)
+               m->do_callback(this, m->user_data());
+       return;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiChannel::resize(int X, int Y, int W, int H)
+{
+       geChannel::resize(X, Y, W, H);
+
+       arm->hide();
+#ifdef WITH_VST
+       fx->hide();
+#endif
+
+       if (w() > BREAK_ARM)
+               arm->show();
+#ifdef WITH_VST
+       if (w() > BREAK_FX)
+               fx->show();
+#endif
+
+       packWidgets();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/mainWindow/keyboard/midiChannel.h b/src/gui/elems/mainWindow/keyboard/midiChannel.h
new file mode 100644 (file)
index 0000000..9fc4db3
--- /dev/null
@@ -0,0 +1,55 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_MIDI_CHANNEL_H
+#define GE_MIDI_CHANNEL_H
+
+#include "channel.h"
+#include "channelButton.h"
+
+namespace giada
+{
+namespace v
+{
+class geMidiChannel : public geChannel
+{
+public:
+       geMidiChannel(int x, int y, int w, int h, c::channel::Data d);
+
+       void resize(int x, int y, int w, int h) override;
+
+  private:
+       static void cb_playButton(Fl_Widget* /*w*/, void* p);
+       static void cb_openMenu(Fl_Widget* /*w*/, void* p);
+       void        cb_playButton();
+       void        cb_openMenu();
+
+       c::channel::Data m_data;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/midiChannelButton.cpp b/src/gui/elems/mainWindow/keyboard/midiChannelButton.cpp
new file mode 100644 (file)
index 0000000..4c5cca9
--- /dev/null
@@ -0,0 +1,66 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiChannelButton.h"
+#include "glue/channel.h"
+#include "utils/string.h"
+
+namespace giada
+{
+namespace v
+{
+geMidiChannelButton::geMidiChannelButton(int x, int y, int w, int h, const c::channel::Data& d)
+: geChannelButton(x, y, w, h, d)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiChannelButton::refresh()
+{
+       geChannelButton::refresh();
+
+       refreshLabel();
+
+       if (m_channel.isRecordingAction() && m_channel.isArmed())
+               setActionRecordMode();
+
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiChannelButton::refreshLabel()
+{
+       std::string l = m_channel.name.empty() ? "-- MIDI --" : m_channel.name;
+
+       if (m_channel.midi->isOutputEnabled())
+               l += " (ch " + std::to_string(m_channel.midi->getFilter() + 1) + " out)";
+
+       copy_label(l.c_str());
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/mainWindow/keyboard/midiChannelButton.h b/src/gui/elems/mainWindow/keyboard/midiChannelButton.h
new file mode 100644 (file)
index 0000000..3337b5a
--- /dev/null
@@ -0,0 +1,49 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_MIDI_CHANNEL_BUTTON_H
+#define GE_MIDI_CHANNEL_BUTTON_H
+
+#include "channelButton.h"
+
+namespace giada
+{
+namespace v
+{
+class geMidiChannelButton : public geChannelButton
+{
+public:
+       geMidiChannelButton(int x, int y, int w, int h, const c::channel::Data& d);
+
+       void refresh() override;
+
+  private:
+       void refreshLabel();
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/sampleChannel.cpp b/src/gui/elems/mainWindow/keyboard/sampleChannel.cpp
new file mode 100644 (file)
index 0000000..f2759e4
--- /dev/null
@@ -0,0 +1,390 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/keyboard/sampleChannel.h"
+#include "core/graphics.h"
+#include "glue/channel.h"
+#include "glue/events.h"
+#include "glue/io.h"
+#include "glue/layout.h"
+#include "glue/recorder.h"
+#include "glue/storage.h"
+#include "gui/dispatcher.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/statusButton.h"
+#include "gui/elems/mainWindow/keyboard/channelMode.h"
+#include "gui/elems/mainWindow/keyboard/channelStatus.h"
+#include "gui/elems/mainWindow/keyboard/column.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/mainWindow/keyboard/midiActivity.h"
+#include "gui/elems/mainWindow/keyboard/sampleChannelButton.h"
+#include "utils/gui.h"
+
+namespace giada::v
+{
+namespace
+{
+enum class Menu
+{
+       INPUT_MONITOR = 0,
+       OVERDUB_PROTECTION,
+       LOAD_SAMPLE,
+       EXPORT_SAMPLE,
+       SETUP_KEYBOARD_INPUT,
+       SETUP_MIDI_INPUT,
+       SETUP_MIDI_OUTPUT,
+       EDIT_SAMPLE,
+       EDIT_ACTIONS,
+       CLEAR_ACTIONS,
+       CLEAR_ACTIONS_ALL,
+       CLEAR_ACTIONS_VOLUME,
+       CLEAR_ACTIONS_START_STOP,
+       __END_CLEAR_ACTIONS_SUBMENU__,
+       RENAME_CHANNEL,
+       CLONE_CHANNEL,
+       FREE_CHANNEL,
+       DELETE_CHANNEL
+};
+
+/* -------------------------------------------------------------------------- */
+
+void menuCallback(Fl_Widget* w, void* v)
+{
+       const geSampleChannel*  gch  = static_cast<geSampleChannel*>(w);
+       const c::channel::Data& data = gch->getData();
+
+       switch ((Menu)(intptr_t)v)
+       {
+       case Menu::INPUT_MONITOR:
+       {
+               c::channel::setInputMonitor(data.id, !data.sample->getInputMonitor());
+               break;
+       }
+       case Menu::OVERDUB_PROTECTION:
+       {
+               c::channel::setOverdubProtection(data.id, !data.sample->getOverdubProtection());
+               break;
+       }
+       case Menu::LOAD_SAMPLE:
+       {
+               c::layout::openBrowserForSampleLoad(data.id);
+               break;
+       }
+       case Menu::EXPORT_SAMPLE:
+       {
+               c::layout::openBrowserForSampleSave(data.id);
+               break;
+       }
+       case Menu::SETUP_KEYBOARD_INPUT:
+       {
+               c::layout::openKeyGrabberWindow(data.key, [channelId = data.id](int key) {
+                       return c::io::channel_setKey(channelId, key);
+               });
+               break;
+       }
+       case Menu::SETUP_MIDI_INPUT:
+       {
+               c::layout::openChannelMidiInputWindow(data.id);
+               break;
+       }
+       case Menu::SETUP_MIDI_OUTPUT:
+       {
+               c::layout::openSampleChannelMidiOutputWindow(data.id);
+               break;
+       }
+       case Menu::EDIT_SAMPLE:
+       {
+               c::layout::openSampleEditor(data.id);
+               break;
+       }
+       case Menu::EDIT_ACTIONS:
+       {
+               c::layout::openSampleActionEditor(data.id);
+               break;
+       }
+       case Menu::CLEAR_ACTIONS:
+       case Menu::__END_CLEAR_ACTIONS_SUBMENU__:
+               break;
+       case Menu::CLEAR_ACTIONS_ALL:
+       {
+               c::recorder::clearAllActions(data.id);
+               break;
+       }
+       case Menu::CLEAR_ACTIONS_VOLUME:
+       {
+               c::recorder::clearVolumeActions(data.id);
+               break;
+       }
+       case Menu::CLEAR_ACTIONS_START_STOP:
+       {
+               c::recorder::clearStartStopActions(data.id);
+               break;
+       }
+       case Menu::CLONE_CHANNEL:
+       {
+               c::channel::cloneChannel(data.id);
+               break;
+       }
+       case Menu::RENAME_CHANNEL:
+       {
+               c::layout::openRenameChannelWindow(data);
+               break;
+       }
+       case Menu::FREE_CHANNEL:
+       {
+               c::channel::freeChannel(data.id);
+               break;
+       }
+       case Menu::DELETE_CHANNEL:
+       {
+               c::channel::deleteChannel(data.id);
+               break;
+       }
+       }
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geSampleChannel::geSampleChannel(int X, int Y, int W, int H, c::channel::Data d)
+: geChannel(X, Y, W, H, d)
+{
+       playButton   = new geStatusButton(x(), y(), G_GUI_UNIT, G_GUI_UNIT, channelStop_xpm, channelPlay_xpm);
+       arm          = new geButton(playButton->x() + playButton->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, "", armOff_xpm, armOn_xpm, armDisabled_xpm);
+       status       = new geChannelStatus(arm->x() + arm->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, h(), m_channel);
+       mainButton   = new geSampleChannelButton(status->x() + status->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, H, m_channel);
+       midiActivity = new geMidiActivity(mainButton->x() + mainButton->w() + G_GUI_INNER_MARGIN, y(), 10, h());
+       readActions  = new geStatusButton(midiActivity->x() + midiActivity->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, readActionOff_xpm, readActionOn_xpm, readActionDisabled_xpm);
+       modeBox      = new geChannelMode(readActions->x() + readActions->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, m_channel);
+       mute         = new geStatusButton(modeBox->x() + modeBox->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, muteOff_xpm, muteOn_xpm);
+       solo         = new geStatusButton(mute->x() + mute->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, soloOff_xpm, soloOn_xpm);
+#if defined(WITH_VST)
+       fx  = new geStatusButton(solo->x() + solo->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT, fxOff_xpm, fxOn_xpm);
+       vol = new geDial(fx->x() + fx->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT);
+#else
+       vol = new geDial(solo->x() + solo->w() + G_GUI_INNER_MARGIN, y(), G_GUI_UNIT, G_GUI_UNIT);
+#endif
+
+       end();
+
+       resizable(mainButton);
+
+       playButton->copy_tooltip("Play/stop");
+       arm->copy_tooltip("Arm for recording");
+       status->copy_tooltip("Progress bar");
+       readActions->copy_tooltip("Read actions\n\nToggles playback of pre-recorded "
+                                 "actions (key press, key release, ...).");
+       modeBox->copy_tooltip("Mode");
+       mute->copy_tooltip("Mute");
+       solo->copy_tooltip("Solo");
+#if defined(WITH_VST)
+       fx->copy_tooltip("Plug-ins");
+#endif
+       vol->copy_tooltip("Volume");
+
+#ifdef WITH_VST
+       fx->setStatus(m_channel.plugins.size() > 0);
+#endif
+
+       playButton->callback(cb_playButton, (void*)this);
+       playButton->when(FL_WHEN_CHANGED); // On keypress && on keyrelease
+
+       arm->type(FL_TOGGLE_BUTTON);
+       arm->value(m_channel.isArmed());
+       arm->callback(cb_arm, (void*)this);
+
+#ifdef WITH_VST
+       fx->callback(cb_openFxWindow, (void*)this);
+#endif
+
+       mute->type(FL_TOGGLE_BUTTON);
+       mute->callback(cb_mute, (void*)this);
+
+       solo->type(FL_TOGGLE_BUTTON);
+       solo->callback(cb_solo, (void*)this);
+
+       mainButton->callback(cb_openMenu, (void*)this);
+
+       readActions->callback(cb_readActions, (void*)this);
+
+       vol->value(m_channel.volume);
+       vol->callback(cb_changeVol, (void*)this);
+
+       size(w(), h()); // Force responsiveness
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleChannel::cb_playButton(Fl_Widget* /*w*/, void* p) { ((geSampleChannel*)p)->cb_playButton(); }
+void geSampleChannel::cb_openMenu(Fl_Widget* /*w*/, void* p) { ((geSampleChannel*)p)->cb_openMenu(); }
+void geSampleChannel::cb_readActions(Fl_Widget* /*w*/, void* p) { ((geSampleChannel*)p)->cb_readActions(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleChannel::cb_playButton()
+{
+       m_channel.viewDispatcher.dispatchTouch(*this, playButton->value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleChannel::cb_openMenu()
+{
+       /* If you're recording (input or actions) no menu is allowed; you can't do
+       anything, especially deallocate the channel. */
+
+       if (m_channel.isRecordingAction() || m_channel.isRecordingInput())
+               return;
+
+       Fl_Menu_Item rclick_menu[] = {
+           u::gui::makeMenuItem("Input monitor", menuCallback, (void*)Menu::INPUT_MONITOR,
+               FL_MENU_TOGGLE | (m_channel.sample->getInputMonitor() ? FL_MENU_VALUE : 0)),
+           u::gui::makeMenuItem("Overdub protection", menuCallback, (void*)Menu::OVERDUB_PROTECTION,
+               FL_MENU_TOGGLE | FL_MENU_DIVIDER | (m_channel.sample->getOverdubProtection() ? FL_MENU_VALUE : 0)),
+           u::gui::makeMenuItem("Load new sample...", menuCallback, (void*)Menu::LOAD_SAMPLE),
+           u::gui::makeMenuItem("Export sample to file...", menuCallback, (void*)Menu::EXPORT_SAMPLE),
+           u::gui::makeMenuItem("Setup keyboard input...", menuCallback, (void*)Menu::SETUP_KEYBOARD_INPUT),
+           u::gui::makeMenuItem("Setup MIDI input...", menuCallback, (void*)Menu::SETUP_MIDI_INPUT),
+           u::gui::makeMenuItem("Setup MIDI output...", menuCallback, (void*)Menu::SETUP_MIDI_OUTPUT),
+           u::gui::makeMenuItem("Edit sample...", menuCallback, (void*)Menu::EDIT_SAMPLE),
+           u::gui::makeMenuItem("Edit actions...", menuCallback, (void*)Menu::EDIT_ACTIONS),
+           u::gui::makeMenuItem("Clear actions", menuCallback, (void*)Menu::CLEAR_ACTIONS, FL_SUBMENU),
+           u::gui::makeMenuItem("All", menuCallback, (void*)Menu::CLEAR_ACTIONS_ALL),
+           u::gui::makeMenuItem("Volume", menuCallback, (void*)Menu::CLEAR_ACTIONS_VOLUME),
+           u::gui::makeMenuItem("Start/Stop", menuCallback, (void*)Menu::CLEAR_ACTIONS_START_STOP),
+           {},
+           u::gui::makeMenuItem("Rename", menuCallback, (void*)Menu::RENAME_CHANNEL),
+           u::gui::makeMenuItem("Clone", menuCallback, (void*)Menu::CLONE_CHANNEL),
+           u::gui::makeMenuItem("Free", menuCallback, (void*)Menu::FREE_CHANNEL),
+           u::gui::makeMenuItem("Delete", menuCallback, (void*)Menu::DELETE_CHANNEL),
+           {}};
+
+       if (m_channel.sample->waveId == 0)
+       {
+               rclick_menu[(int)Menu::EXPORT_SAMPLE].deactivate();
+               rclick_menu[(int)Menu::EDIT_SAMPLE].deactivate();
+               rclick_menu[(int)Menu::FREE_CHANNEL].deactivate();
+               rclick_menu[(int)Menu::RENAME_CHANNEL].deactivate();
+       }
+
+       if (!m_channel.hasActions)
+               rclick_menu[(int)Menu::CLEAR_ACTIONS].deactivate();
+
+       /* No 'clear start/stop actions' for those channels in loop mode: they cannot
+       have start/stop actions. */
+
+       if (m_channel.sample->isLoop)
+               rclick_menu[(int)Menu::CLEAR_ACTIONS_START_STOP].deactivate();
+
+       Fl_Menu_Button b(0, 0, 100, 50);
+       b.box(G_CUSTOM_BORDER_BOX);
+       b.textsize(G_GUI_FONT_SIZE_BASE);
+       b.textcolor(G_COLOR_LIGHT_2);
+       b.color(G_COLOR_GREY_2);
+
+       const Fl_Menu_Item* m = rclick_menu->popup(Fl::event_x(), Fl::event_y(), 0, 0, &b);
+       if (m != nullptr)
+               m->do_callback(this, m->user_data());
+       return;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleChannel::cb_readActions()
+{
+       if (Fl::event_shift())
+               c::events::killReadActionsChannel(m_channel.id, Thread::MAIN);
+       else
+               c::events::toggleReadActionsChannel(m_channel.id, Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleChannel::refresh()
+{
+       geChannel::refresh();
+
+       if (m_channel.sample->waveId != 0)
+       {
+               status->redraw();
+               if (m_channel.sample->getOverdubProtection())
+                       arm->deactivate();
+               else
+                       arm->activate();
+       }
+
+       if (m_channel.hasActions)
+       {
+               readActions->activate();
+               readActions->setStatus(m_channel.getReadActions());
+       }
+       else
+               readActions->deactivate();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleChannel::draw()
+{
+       const int ny = y() + (h() / 2) - (G_GUI_UNIT / 2);
+
+       modeBox->resize(modeBox->x(), ny, G_GUI_UNIT, G_GUI_UNIT);
+       readActions->resize(readActions->x(), ny, G_GUI_UNIT, G_GUI_UNIT);
+
+       geChannel::draw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleChannel::resize(int X, int Y, int W, int H)
+{
+       geChannel::resize(X, Y, W, H);
+
+       arm->hide();
+       modeBox->hide();
+       readActions->hide();
+#ifdef WITH_VST
+       fx->hide();
+#endif
+
+       if (w() > BREAK_ARM)
+               arm->show();
+#ifdef WITH_VST
+       if (w() > BREAK_FX)
+               fx->show();
+#endif
+       if (w() > BREAK_MODE_BOX)
+               modeBox->show();
+       if (w() > BREAK_READ_ACTIONS)
+               readActions->show();
+
+       packWidgets();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/mainWindow/keyboard/sampleChannel.h b/src/gui/elems/mainWindow/keyboard/sampleChannel.h
new file mode 100644 (file)
index 0000000..f1f6d36
--- /dev/null
@@ -0,0 +1,60 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SAMPLE_CHANNEL_H
+#define GE_SAMPLE_CHANNEL_H
+
+#include "channel.h"
+#include "glue/channel.h"
+
+namespace giada::v
+{
+class geStatusButton;
+class geChannelMode;
+class geSampleChannel : public geChannel
+{
+public:
+       geSampleChannel(int x, int y, int w, int h, c::channel::Data d);
+
+       void resize(int x, int y, int w, int h) override;
+       void draw() override;
+
+       void refresh() override;
+
+       geChannelMode*  modeBox;
+       geStatusButton* readActions;
+
+private:
+       static void cb_playButton(Fl_Widget* /*w*/, void* p);
+       static void cb_openMenu(Fl_Widget* /*w*/, void* p);
+       static void cb_readActions(Fl_Widget* /*w*/, void* p);
+       void        cb_playButton();
+       void        cb_openMenu();
+       void        cb_readActions();
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/keyboard/sampleChannelButton.cpp b/src/gui/elems/mainWindow/keyboard/sampleChannelButton.cpp
new file mode 100644 (file)
index 0000000..e319ac3
--- /dev/null
@@ -0,0 +1,90 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "sampleChannelButton.h"
+#include "glue/channel.h"
+#include "gui/dialogs/mainWindow.h"
+#include "keyboard.h"
+#include "sampleChannel.h"
+#include "utils/fs.h"
+#include "utils/string.h"
+#include <FL/Fl.H>
+
+namespace giada::v
+{
+geSampleChannelButton::geSampleChannelButton(int x, int y, int w, int h, const c::channel::Data& d)
+: geChannelButton(x, y, w, h, d)
+{
+       switch (m_channel.getPlayStatus())
+       {
+       case ChannelStatus::MISSING:
+       case ChannelStatus::WRONG:
+               label("* file not found! *");
+               break;
+       default:
+               label(m_channel.sample->waveId == 0 ? "-- no sample --" : m_channel.name.c_str());
+               break;
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSampleChannelButton::refresh()
+{
+       geChannelButton::refresh();
+
+       if (m_channel.isRecordingInput() && m_channel.isArmed())
+               setInputRecordMode();
+       else if (m_channel.isRecordingAction() && m_channel.sample->waveId != 0 && !m_channel.sample->isLoop)
+               setActionRecordMode();
+
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geSampleChannelButton::handle(int e)
+{
+       int ret = geButton::handle(e);
+       switch (e)
+       {
+       case FL_DND_ENTER:
+       case FL_DND_DRAG:
+       case FL_DND_RELEASE:
+       {
+               ret = 1;
+               break;
+       }
+       case FL_PASTE:
+       {
+               c::channel::loadChannel(m_channel.id, u::string::trim(u::fs::stripFileUrl(Fl::event_text())));
+               ret = 1;
+               break;
+       }
+       }
+       return ret;
+}
+} // namespace giada::v
diff --git a/src/gui/elems/mainWindow/keyboard/sampleChannelButton.h b/src/gui/elems/mainWindow/keyboard/sampleChannelButton.h
new file mode 100644 (file)
index 0000000..b09737a
--- /dev/null
@@ -0,0 +1,48 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SAMPLE_CHANNEL_BUTTON_H
+#define GE_SAMPLE_CHANNEL_BUTTON_H
+
+#include "channelButton.h"
+
+namespace giada
+{
+namespace v
+{
+class geSampleChannelButton : public geChannelButton
+{
+public:
+       geSampleChannelButton(int x, int y, int w, int h, const c::channel::Data& d);
+
+       int handle(int e) override;
+
+       void refresh() override;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/elems/mainWindow/mainIO.cpp b/src/gui/elems/mainWindow/mainIO.cpp
new file mode 100644 (file)
index 0000000..8b3fca8
--- /dev/null
@@ -0,0 +1,147 @@
+/* -----------------------------------------------------------------------------
+*
+* Giada - Your Hardcore Loopmachine
+*
+* ------------------------------------------------------------------------------
+*
+* Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+*
+* This file is part of Giada - Your Hardcore Loopmachine.
+*
+* Giada - Your Hardcore Loopmachine 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.
+*
+* Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+* <http://www.gnu.org/licenses/>.
+*
+* --------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/mainIO.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "glue/channel.h"
+#include "glue/events.h"
+#include "glue/layout.h"
+#include "glue/main.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/statusButton.h"
+#include "gui/elems/soundMeter.h"
+#include "utils/gui.h"
+#ifdef WITH_VST
+#include "gui/elems/basics/statusButton.h"
+#endif
+
+namespace giada::v
+{
+geMainIO::geMainIO(int x, int y, int w, int h)
+: geFlex(x, y, w, h, Direction::HORIZONTAL, G_GUI_INNER_MARGIN)
+{
+       m_outMeter = new geSoundMeter(0, 0, 0, 0);
+       m_inMeter  = new geSoundMeter(0, 0, 0, 0);
+       m_outVol   = new geDial(0, 0, 0, 0);
+       m_inVol    = new geDial(0, 0, 0, 0);
+       m_inToOut  = new geButton();
+#ifdef WITH_VST
+       m_masterFxOut = new geStatusButton(0, 0, 0, 0, fxOff_xpm, fxOn_xpm);
+       m_masterFxIn  = new geStatusButton(0, 0, 0, 0, fxOff_xpm, fxOn_xpm);
+#endif
+
+#ifdef WITH_VST
+       add(m_masterFxIn, G_GUI_UNIT);
+#endif
+       add(m_inVol, G_GUI_UNIT);
+       add(m_inMeter);
+       add(m_inToOut, 12);
+       add(m_outMeter);
+       add(m_outVol, G_GUI_UNIT);
+#ifdef WITH_VST
+       add(m_masterFxOut, G_GUI_UNIT);
+#endif
+       end();
+
+       m_outMeter->copy_tooltip("Main output meter");
+       m_inMeter->copy_tooltip("Main input meter");
+       m_outVol->copy_tooltip("Main output volume");
+       m_inVol->copy_tooltip("Main input volume");
+       m_inToOut->copy_tooltip("Stream linker\n\nConnects input to output to enable \"hear what you're playing\" mode.");
+#ifdef WITH_VST
+       m_masterFxOut->copy_tooltip("Main output plug-ins");
+       m_masterFxIn->copy_tooltip("Main input plug-ins");
+#endif
+
+       m_outVol->onChange = [](float v) {
+               c::events::setMasterOutVolume(v, Thread::MAIN);
+       };
+
+       m_inVol->onChange = [](float v) {
+               c::events::setMasterInVolume(v, Thread::MAIN);
+       };
+
+       m_inToOut->type(FL_TOGGLE_BUTTON);
+       m_inToOut->onClick = [v = m_inToOut->value()]() {
+               c::main::setInToOut(v);
+       };
+
+#ifdef WITH_VST
+       m_masterFxOut->onClick = [] { c::layout::openMasterOutPluginListWindow(); };
+       m_masterFxIn->onClick  = [] { c::layout::openMasterInPluginListWindow(); };
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainIO::setOutVol(float v) { m_outVol->value(v); }
+void geMainIO::setInVol(float v) { m_inVol->value(v); }
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void geMainIO::setMasterFxOutFull(bool v)
+{
+       m_masterFxOut->setStatus(v);
+}
+
+void geMainIO::setMasterFxInFull(bool v)
+{
+       m_masterFxIn->setStatus(v);
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void geMainIO::refresh()
+{
+       m_outMeter->peak  = m_io.getMasterOutPeak();
+       m_outMeter->ready = m_io.isKernelReady();
+       m_inMeter->peak   = m_io.getMasterInPeak();
+       m_inMeter->ready  = m_io.isKernelReady();
+       m_outMeter->redraw();
+       m_inMeter->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainIO::rebuild()
+{
+       m_io = c::main::getIO();
+
+       m_outVol->value(m_io.masterOutVol);
+       m_inVol->value(m_io.masterInVol);
+#ifdef WITH_VST
+       m_masterFxOut->setStatus(m_io.masterOutHasPlugins);
+       m_masterFxIn->setStatus(m_io.masterInHasPlugins);
+       m_inToOut->value(m_io.inToOut);
+#endif
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/mainWindow/mainIO.h b/src/gui/elems/mainWindow/mainIO.h
new file mode 100644 (file)
index 0000000..9b83ccc
--- /dev/null
@@ -0,0 +1,69 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_MAIN_IO_H
+#define GE_MAIN_IO_H
+
+#include "glue/main.h"
+#include "gui/elems/basics/flex.h"
+
+namespace giada::v
+{
+class geDial;
+class geSoundMeter;
+class geButton;
+class geStatusButton;
+class geMainIO : public geFlex
+{
+public:
+       geMainIO(int x, int y, int w, int h);
+
+       void refresh();
+       void rebuild();
+
+       void setOutVol(float v);
+       void setInVol(float v);
+#ifdef WITH_VST
+       void setMasterFxOutFull(bool v);
+       void setMasterFxInFull(bool v);
+#endif
+
+private:
+       c::main::IO m_io;
+
+       geSoundMeter* m_outMeter;
+       geSoundMeter* m_inMeter;
+       geDial*       m_outVol;
+       geDial*       m_inVol;
+       geButton*     m_inToOut;
+#ifdef WITH_VST
+       geStatusButton* m_masterFxOut;
+       geStatusButton* m_masterFxIn;
+#endif
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/mainMenu.cpp b/src/gui/elems/mainWindow/mainMenu.cpp
new file mode 100644 (file)
index 0000000..0823313
--- /dev/null
@@ -0,0 +1,145 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/mainMenu.h"
+#include "core/const.h"
+#include "core/patch.h"
+#include "glue/layout.h"
+#include "glue/main.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/button.h"
+#include "keyboard/keyboard.h"
+#include "utils/gui.h"
+#include <FL/Fl_Menu_Button.H>
+
+namespace giada::v
+{
+geMainMenu::geMainMenu(int x, int y)
+: gePack(x, y, Direction::HORIZONTAL, G_GUI_INNER_MARGIN)
+{
+       geButton* file   = new geButton(0, 0, 70, G_GUI_UNIT, "File");
+       geButton* edit   = new geButton(0, 0, 70, G_GUI_UNIT, "Edit");
+       geButton* config = new geButton(0, 0, 70, G_GUI_UNIT, "Config");
+       geButton* about  = new geButton(0, 0, 70, G_GUI_UNIT, "About");
+       add(file);
+       add(edit);
+       add(config);
+       add(about);
+
+       resizable(nullptr);
+
+       file->onClick   = [this]() { cb_file(); };
+       edit->onClick   = [this]() { cb_edit(); };
+       about->onClick  = []() { c::layout::openAboutWindow(); };
+       config->onClick = []() { c::layout::openConfigWindow(); };
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainMenu::cb_file()
+{
+       Fl_Menu_Item menu[] = {
+           u::gui::makeMenuItem("Open project..."),
+           u::gui::makeMenuItem("Save project..."),
+           u::gui::makeMenuItem("Close project"),
+#ifndef NDEBUG
+           u::gui::makeMenuItem("Debug stats"),
+#endif
+           u::gui::makeMenuItem("Quit Giada"),
+           {}};
+
+       Fl_Menu_Button b(0, 0, 100, 50);
+       b.box(G_CUSTOM_BORDER_BOX);
+       b.textsize(G_GUI_FONT_SIZE_BASE);
+       b.textcolor(G_COLOR_LIGHT_2);
+       b.color(G_COLOR_GREY_2);
+
+       const Fl_Menu_Item* m = menu->popup(Fl::event_x(), Fl::event_y(), 0, 0, &b);
+       if (!m)
+               return;
+
+       if (strcmp(m->label(), "Open project...") == 0)
+       {
+               c::layout::openBrowserForProjectLoad();
+       }
+       else if (strcmp(m->label(), "Save project...") == 0)
+       {
+               c::layout::openBrowserForProjectSave();
+       }
+       else if (strcmp(m->label(), "Close project") == 0)
+       {
+               c::main::closeProject();
+       }
+#ifdef G_DEBUG_MODE
+       else if (strcmp(m->label(), "Debug stats") == 0)
+       {
+               c::main::printDebugInfo();
+       }
+#endif
+       else if (strcmp(m->label(), "Quit Giada") == 0)
+       {
+               c::main::quitGiada();
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainMenu::cb_edit()
+{
+       c::main::MainMenu menu = c::main::getMainMenu();
+
+       Fl_Menu_Item menuItem[] = {
+           u::gui::makeMenuItem("Free all Sample channels"),
+           u::gui::makeMenuItem("Clear all actions"),
+           u::gui::makeMenuItem("Setup global MIDI input..."),
+           {}};
+
+       menuItem[0].deactivate();
+       menuItem[1].deactivate();
+
+       if (menu.hasAudioData)
+               menuItem[0].activate();
+       if (menu.hasActions)
+               menuItem[1].activate();
+
+       Fl_Menu_Button b(0, 0, 100, 50);
+       b.box(G_CUSTOM_BORDER_BOX);
+       b.textsize(G_GUI_FONT_SIZE_BASE);
+       b.textcolor(G_COLOR_LIGHT_2);
+       b.color(G_COLOR_GREY_2);
+
+       const Fl_Menu_Item* m = menuItem->popup(Fl::event_x(), Fl::event_y(), 0, 0, &b);
+       if (!m)
+               return;
+
+       if (strcmp(m->label(), "Free all Sample channels") == 0)
+               c::main::clearAllSamples();
+       else if (strcmp(m->label(), "Clear all actions") == 0)
+               c::main::clearAllActions();
+       else if (strcmp(m->label(), "Setup global MIDI input...") == 0)
+               c::layout::openMasterMidiInputWindow();
+}
+} // namespace giada::v
diff --git a/src/gui/elems/mainWindow/mainMenu.h b/src/gui/elems/mainWindow/mainMenu.h
new file mode 100644 (file)
index 0000000..4ca3823
--- /dev/null
@@ -0,0 +1,45 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_MAIN_MENU_H
+#define GE_MAIN_MENU_H
+
+#include "gui/elems/basics/pack.h"
+
+namespace giada::v
+{
+class geMainMenu : public gePack
+{
+public:
+       geMainMenu(int x, int y);
+
+private:
+       void cb_file();
+       void cb_edit();
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/mainTimer.cpp b/src/gui/elems/mainWindow/mainTimer.cpp
new file mode 100644 (file)
index 0000000..737b475
--- /dev/null
@@ -0,0 +1,157 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "mainTimer.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "glue/events.h"
+#include "glue/layout.h"
+#include "glue/main.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/choice.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+
+namespace giada::v
+{
+geMainTimer::geMainTimer(int x, int y)
+: gePack(x, y, Direction::HORIZONTAL)
+{
+       m_bpm        = new geButton(0, 0, 60, G_GUI_UNIT);
+       m_meter      = new geButton(0, 0, 60, G_GUI_UNIT);
+       m_quantizer  = new geChoice(0, 0, 60, G_GUI_UNIT);
+       m_multiplier = new geButton(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", multiplyOff_xpm, multiplyOn_xpm);
+       m_divider    = new geButton(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", divideOff_xpm, divideOn_xpm);
+       add(m_quantizer);
+       add(m_bpm);
+       add(m_meter);
+       add(m_multiplier);
+       add(m_divider);
+
+       resizable(nullptr); // don't resize any widget
+
+       m_bpm->copy_tooltip("Beats per minute (BPM)");
+       m_meter->copy_tooltip("Beats and bars");
+       m_quantizer->copy_tooltip("Live quantizer");
+       m_multiplier->copy_tooltip("Beat multiplier");
+       m_divider->copy_tooltip("Beat divider");
+
+       m_bpm->onClick        = [&bpm = m_bpm]() { c::layout::openBpmWindow(bpm->label()); };
+       m_meter->onClick      = [&timer = m_timer]() { c::layout::openBeatsWindow(timer.beats, timer.bars); };
+       m_multiplier->onClick = []() { c::events::multiplyBeats(); };
+       m_divider->onClick    = []() { c::events::divideBeats(); };
+
+       m_quantizer->addItem("off");
+       m_quantizer->addItem("1\\/1");
+       m_quantizer->addItem("1\\/2");
+       m_quantizer->addItem("1\\/3");
+       m_quantizer->addItem("1\\/4");
+       m_quantizer->addItem("1\\/6");
+       m_quantizer->addItem("1\\/8");
+       m_quantizer->showItem(1); //  "off" by default
+       m_quantizer->onChange = [](ID value) { c::main::quantize(value); };
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainTimer::refresh()
+{
+       m_timer = c::main::getTimer();
+
+       if (m_timer.isRecordingInput)
+       {
+               m_bpm->deactivate();
+               m_meter->deactivate();
+               m_multiplier->deactivate();
+               m_divider->deactivate();
+       }
+       else
+       {
+               m_bpm->activate();
+               m_meter->activate();
+               m_multiplier->activate();
+               m_divider->activate();
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainTimer::rebuild()
+{
+       m_timer = c::main::getTimer();
+
+       setBpm(m_timer.bpm);
+       setMeter(m_timer.beats, m_timer.bars);
+       setQuantizer(m_timer.quantize);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainTimer::setBpm(const char* v)
+{
+       m_bpm->copy_label(v);
+}
+
+void geMainTimer::setBpm(float v)
+{
+       m_bpm->copy_label(u::string::fToString(v, 1).c_str()); // Only 1 decimal place (e.g. 120.0)
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainTimer::setLock(bool v)
+{
+       if (v)
+       {
+               m_bpm->deactivate();
+               m_meter->deactivate();
+               m_multiplier->deactivate();
+               m_divider->deactivate();
+       }
+       else
+       {
+               m_bpm->activate();
+               m_meter->activate();
+               m_multiplier->activate();
+               m_divider->activate();
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainTimer::setQuantizer(int q)
+{
+       m_quantizer->showItem(q);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainTimer::setMeter(int beats, int bars)
+{
+       std::string s = std::to_string(beats) + "/" + std::to_string(bars);
+       m_meter->copy_label(s.c_str());
+}
+} // namespace giada::v
diff --git a/src/gui/elems/mainWindow/mainTimer.h b/src/gui/elems/mainWindow/mainTimer.h
new file mode 100644 (file)
index 0000000..c1ee8d5
--- /dev/null
@@ -0,0 +1,66 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_MAIN_TIMER_H
+#define GE_MAIN_TIMER_H
+
+#include "glue/main.h"
+#include "gui/elems/basics/pack.h"
+
+namespace giada::v
+{
+class geButton;
+class geChoice;
+class geMainTimer : public gePack
+{
+public:
+       geMainTimer(int x, int y);
+
+       void refresh();
+       void rebuild();
+
+       void setBpm(const char* v);
+       void setBpm(float v);
+       void setMeter(int beats, int bars);
+       void setQuantizer(int q);
+
+       /* setLock
+       Locks bpm, meter and multipliers. Used during audio recordings. */
+
+       void setLock(bool v);
+
+private:
+       c::main::Timer m_timer;
+
+       geButton* m_bpm;
+       geButton* m_meter;
+       geChoice* m_quantizer;
+       geButton* m_multiplier;
+       geButton* m_divider;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/mainTransport.cpp b/src/gui/elems/mainWindow/mainTransport.cpp
new file mode 100644 (file)
index 0000000..c0d680a
--- /dev/null
@@ -0,0 +1,114 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/mainWindow/mainTransport.h"
+#include "core/conf.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "glue/events.h"
+#include "glue/main.h"
+
+namespace giada::v
+{
+geMainTransport::geMainTransport(int x, int y)
+: gePack(x, y, Direction::HORIZONTAL)
+, m_rewind(0, 0, 25, 25, "", rewindOff_xpm, rewindOn_xpm)
+, m_play(0, 0, 25, 25, play_xpm, pause_xpm)
+, m_spacer1(0, 0, 10, 25)
+, m_recTriggerMode(0, 0, 15, 25, recTriggerModeOff_xpm, recTriggerModeOn_xpm)
+, m_recAction(0, 0, 25, 25, recOff_xpm, recOn_xpm)
+, m_recInput(0, 0, 25, 25, inputRecOff_xpm, inputRecOn_xpm)
+, m_inputRecMode(0, 0, 15, 25, freeInputRecOff_xpm, freeInputRecOn_xpm)
+, m_spacer2(0, 0, 10, 25)
+, m_metronome(0, 0, 15, 25, metronomeOff_xpm, metronomeOn_xpm)
+{
+       add(&m_rewind);
+       add(&m_play);
+       add(&m_spacer1);
+       add(&m_recTriggerMode);
+       add(&m_recAction);
+       add(&m_recInput);
+       add(&m_inputRecMode);
+       add(&m_spacer2);
+       add(&m_metronome);
+
+       m_rewind.copy_tooltip("Rewind");
+       m_play.copy_tooltip("Play/Stop");
+       m_recTriggerMode.copy_tooltip("Record-on-signal mode\n\nIf enabled, action "
+                                     "and audio recording will start only when a signal (key press or audio) "
+                                     "is detected.");
+       m_recAction.copy_tooltip("Record actions");
+       m_recInput.copy_tooltip("Record audio");
+       m_inputRecMode.copy_tooltip("Free loop-length mode\n\nIf enabled, the sequencer "
+                                   "will adjust to the length of your first audio recording. "
+                                   "Available only if there are no other audio samples in the "
+                                   "project.");
+       m_metronome.copy_tooltip("Metronome");
+
+       m_rewind.callback([](Fl_Widget* /*w*/, void* /*v*/) {
+               c::events::rewindSequencer(Thread::MAIN);
+       });
+
+       m_play.callback([](Fl_Widget* /*w*/, void* /*v*/) {
+               c::events::toggleSequencer(Thread::MAIN);
+       });
+
+       m_recAction.callback([](Fl_Widget* /*w*/, void* /*v*/) {
+               c::events::toggleActionRecording();
+       });
+
+       m_recInput.callback([](Fl_Widget* /*w*/, void* /*v*/) {
+               c::events::toggleInputRecording();
+       });
+
+       m_recTriggerMode.callback([](Fl_Widget* /*w*/, void* /*v*/) {
+               c::main::toggleRecOnSignal();
+       });
+
+       m_inputRecMode.callback([](Fl_Widget* /*w*/, void* /*v*/) {
+               c::main::toggleFreeInputRec();
+       });
+
+       m_metronome.type(FL_TOGGLE_BUTTON);
+       m_metronome.callback([](Fl_Widget* /*w*/, void* /*v*/) {
+               c::events::toggleMetronome();
+       });
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMainTransport::refresh()
+{
+       c::main::Transport transport = c::main::getTransport();
+
+       m_play.setStatus(transport.isRunning);
+       m_recAction.setStatus(transport.isRecordingAction);
+       m_recInput.setStatus(transport.isRecordingInput);
+       m_metronome.setStatus(transport.isMetronomeOn);
+       m_recTriggerMode.setStatus(transport.recTriggerMode == RecTriggerMode::SIGNAL);
+       m_inputRecMode.setStatus(transport.inputRecMode == InputRecMode::FREE);
+}
+} // namespace giada::v
diff --git a/src/gui/elems/mainWindow/mainTransport.h b/src/gui/elems/mainWindow/mainTransport.h
new file mode 100644 (file)
index 0000000..f9e97a4
--- /dev/null
@@ -0,0 +1,57 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_MAIN_TRANSPORT_H
+#define GE_MAIN_TRANSPORT_H
+
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/pack.h"
+#include "gui/elems/basics/statusButton.h"
+
+namespace giada::v
+{
+class geMainTransport : public gePack
+{
+public:
+       geMainTransport(int x, int y);
+
+       void refresh();
+
+private:
+       geButton       m_rewind;
+       geStatusButton m_play;
+       geBox          m_spacer1;
+       geStatusButton m_recTriggerMode;
+       geStatusButton m_recAction;
+       geStatusButton m_recInput;
+       geStatusButton m_inputRecMode;
+       geBox          m_spacer2;
+       geStatusButton m_metronome;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/mainWindow/sequencer.cpp b/src/gui/elems/mainWindow/sequencer.cpp
new file mode 100644 (file)
index 0000000..f0e38a5
--- /dev/null
@@ -0,0 +1,127 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * beatMeter
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "sequencer.h"
+#include "core/const.h"
+#include "gui/drawing.h"
+#include "utils/math.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+geSequencer::geSequencer(int x, int y, int w, int h)
+: Fl_Box(x, y, w, h)
+{
+       copy_tooltip("Main sequencer");
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSequencer::refresh()
+{
+       m_data = c::main::getSequencer();
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSequencer::draw()
+{
+       m_background = geompp::Rect(x(), y(), w(), h());
+       m_cell       = geompp::Rect(x(), y(), w() / G_MAX_BEATS, h()).reduced({0, REC_BARS_H});
+
+       /* Cleanup */
+       drawRectf(m_background, FL_BACKGROUND_COLOR);
+
+       if (m_data.isFreeModeInputRec)
+               drawRecBars();
+
+       drawBody();
+       drawCursor();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSequencer::drawBody() const
+{
+       const geompp::Rect body = m_background.reduced({0, REC_BARS_H});
+       const geompp::Line line = m_cell.getHeightAsLine();
+
+       /* Background and borders. */
+
+       drawRectf(body, FL_BACKGROUND_COLOR);
+       drawRect(body, G_COLOR_GREY_4);
+
+       /* Beat lines. */
+
+       for (int i = 1; i <= m_data.beats; i++)
+               drawLine(line.withShiftedX(m_cell.w * i), G_COLOR_GREY_4);
+
+       /* Bar lines. */
+
+       const int delta = m_data.beats / m_data.bars;
+       for (int i = 1; i < m_data.bars; i++)
+               drawLine(line.withShiftedX(m_cell.w * i * delta), G_COLOR_LIGHT_1);
+
+       /* Unused grey area. */
+
+       drawRectf(body.withTrimmedLeft(m_data.beats * m_cell.w), G_COLOR_GREY_4);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSequencer::drawRecBars() const
+{
+       int length = u::math::map(m_data.recPosition, m_data.recMaxLength, w());
+
+       drawRectf(geompp::Rect(x(), y(), length, h()), G_COLOR_LIGHT_1);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSequencer::drawCursor(int beat, Fl_Color color) const
+{
+       // TODO withW(...): FLTK glitch?
+       drawRectf(m_cell.withShiftedX(beat * m_cell.w).reduced(CURSOR_PAD).withW(m_cell.w - CURSOR_PAD - 2), color);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSequencer::drawCursor() const
+{
+       Fl_Color color = m_data.shouldBlink ? FL_BACKGROUND_COLOR : G_COLOR_LIGHT_1;
+
+       if (m_data.isFreeModeInputRec)
+       {
+               for (int i = 0; i < m_data.beats; i++)
+                       drawCursor(i, color);
+       }
+       else
+               drawCursor(m_data.currentBeat, color);
+}
+} // namespace giada::v
diff --git a/src/gui/elems/mainWindow/sequencer.h b/src/gui/elems/mainWindow/sequencer.h
new file mode 100644 (file)
index 0000000..1d6dab2
--- /dev/null
@@ -0,0 +1,64 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * beatMeter
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SEQUENCER_H
+#define GE_SEQUENCER_H
+
+#include "core/types.h"
+#include "deps/geompp/src/rect.hpp"
+#include "glue/main.h"
+#include <FL/Fl_Box.H>
+
+namespace giada::v
+{
+class geSequencer : public Fl_Box
+{
+public:
+       geSequencer(int x, int y, int w, int h);
+
+       void draw() override;
+
+       void refresh();
+
+private:
+       static constexpr int REC_BARS_H = 3;
+       static constexpr int CURSOR_PAD = 3;
+
+       void drawBody() const;
+       void drawCursor() const;
+       void drawCursor(int beat, Fl_Color col) const;
+       void drawRecBars() const;
+
+       c::main::Sequencer m_data;
+
+       geompp::Rect<int> m_background;
+       geompp::Rect<int> m_cell;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/midiIO/midiLearner.cpp b/src/gui/elems/midiIO/midiLearner.cpp
new file mode 100644 (file)
index 0000000..e730c23
--- /dev/null
@@ -0,0 +1,116 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/midiIO/midiLearner.h"
+#include "core/const.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/button.h"
+#include "utils/string.h"
+#include <cassert>
+
+namespace giada::v
+{
+geMidiLearner::geMidiLearner(int x, int y, int w, int h, std::string l, int param)
+: geFlex(x, y, w, h, Direction::HORIZONTAL, G_GUI_INNER_MARGIN)
+, onStartLearn(nullptr)
+, onStopLearn(nullptr)
+, onClearLearn(nullptr)
+, m_param(param)
+{
+       m_text     = new geBox(l.c_str());
+       m_valueBtn = new geButton();
+       m_button   = new geButton("learn");
+
+       add(m_text);
+       add(m_valueBtn, 80);
+       add(m_button, 50);
+       end();
+
+       m_text->box(G_CUSTOM_BORDER_BOX);
+
+       m_valueBtn->box(G_CUSTOM_BORDER_BOX);
+       m_valueBtn->when(FL_WHEN_RELEASE);
+       m_valueBtn->onClick = [this]() {
+               assert(onClearLearn != nullptr);
+
+               if (Fl::event_button() == FL_RIGHT_MOUSE)
+                       onClearLearn(m_param);
+       };
+
+       m_button->type(FL_TOGGLE_BUTTON);
+       m_button->onClick = [this]() {
+               assert(onStartLearn != nullptr);
+               assert(onStopLearn != nullptr);
+
+               if (m_button->value() == 1)
+                       onStartLearn(m_param);
+               else
+                       onStopLearn();
+       };
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiLearner::update(uint32_t value)
+{
+       std::string tmp = "(not set)";
+
+       if (value != 0x0)
+       {
+               tmp = "0x" + u::string::iToString(value, /*hex=*/true);
+               tmp.pop_back(); // Remove last two digits, useless in MIDI messages
+               tmp.pop_back(); // Remove last two digits, useless in MIDI messages
+       }
+
+       m_valueBtn->copy_label(tmp.c_str());
+       m_button->value(0);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiLearner::update(const std::string& s)
+{
+       m_valueBtn->copy_label(s.c_str());
+       m_button->value(0);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiLearner::activate()
+{
+       Fl_Group::activate();
+       m_valueBtn->activate();
+       m_button->activate();
+}
+
+void geMidiLearner::deactivate()
+{
+       Fl_Group::deactivate();
+       m_valueBtn->deactivate();
+       m_button->deactivate();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/midiIO/midiLearner.h b/src/gui/elems/midiIO/midiLearner.h
new file mode 100644 (file)
index 0000000..393c005
--- /dev/null
@@ -0,0 +1,72 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_MIDI_LEARNER_H
+#define GE_MIDI_LEARNER_H
+
+#include "gui/elems/basics/flex.h"
+#include <functional>
+#include <string>
+
+namespace giada::v
+{
+class geBox;
+class geButton;
+class geMidiLearner : public geFlex
+{
+public:
+       geMidiLearner(int x, int y, int w, int h, std::string l, int param);
+
+       /* update
+       Updates and repaints the label widget with value 'value'. */
+
+       void update(uint32_t value);
+
+       /* update (1)
+       Just sets the label widget with a string value (no parsing done as in (1)). */
+
+       void update(const std::string&);
+
+       void activate();
+       void deactivate();
+
+       std::function<void(uint32_t)> onStartLearn;
+       std::function<void()>         onStopLearn;
+       std::function<void(uint32_t)> onClearLearn;
+
+protected:
+       /* m_param
+       Parameter index to be learnt. */
+
+       int m_param;
+
+       geBox*    m_text;
+       geButton* m_valueBtn;
+       geButton* m_button;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/midiIO/midiLearnerPack.cpp b/src/gui/elems/midiIO/midiLearnerPack.cpp
new file mode 100644 (file)
index 0000000..c79b1ef
--- /dev/null
@@ -0,0 +1,90 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "midiLearnerPack.h"
+#include "core/const.h"
+#include "glue/io.h"
+#include "gui/elems/basics/box.h"
+#include <cassert>
+
+namespace giada
+{
+namespace v
+{
+constexpr int LEARNER_WIDTH = 284;
+
+/* -------------------------------------------------------------------------- */
+
+geMidiLearnerPack::geMidiLearnerPack(int X, int Y, std::string title)
+: gePack(X, Y, Direction::VERTICAL)
+{
+       end();
+
+       if (title != "")
+       {
+               geBox* header = new geBox(0, 0, LEARNER_WIDTH, G_GUI_UNIT, title.c_str());
+               header->box(FL_BORDER_BOX);
+               add(header);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiLearnerPack::setCallbacks(std::function<void(uint32_t)> s, std::function<void(uint32_t)> c)
+{
+       m_onStartLearn = s;
+       m_onClearLearn = c;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiLearnerPack::addMidiLearner(std::string label, int param, bool visible)
+{
+       geMidiLearner* l = new geMidiLearner(0, 0, LEARNER_WIDTH, G_GUI_UNIT, label, param);
+
+       l->onStartLearn = m_onStartLearn;
+       l->onClearLearn = m_onClearLearn;
+       l->onStopLearn  = []() { c::io::stopMidiLearn(); };
+
+       add(l);
+       if (!visible)
+               l->hide();
+       learners.push_back(l);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geMidiLearnerPack::setEnabled(bool v)
+{
+       if (v)
+               for (auto* l : learners)
+                       l->activate();
+       else
+               for (auto* l : learners)
+                       l->deactivate();
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/midiIO/midiLearnerPack.h b/src/gui/elems/midiIO/midiLearnerPack.h
new file mode 100644 (file)
index 0000000..1622150
--- /dev/null
@@ -0,0 +1,57 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_LEARNER_PACK_H
+#define GE_LEARNER_PACK_H
+
+#include "gui/elems/basics/pack.h"
+#include "gui/elems/midiIO/midiLearner.h"
+#include <string>
+#include <vector>
+
+namespace giada
+{
+namespace v
+{
+class geMidiLearnerPack : public gePack
+{
+public:
+       geMidiLearnerPack(int x, int y, std::string title = "");
+
+       void setCallbacks(std::function<void(uint32_t)>, std::function<void(uint32_t)>);
+       void addMidiLearner(std::string label, int param, bool visible = true);
+       void setEnabled(bool v);
+
+       std::vector<geMidiLearner*> learners;
+
+  private:
+       std::function<void(uint32_t)> m_onStartLearn;
+       std::function<void(uint32_t)> m_onClearLearn;
+};
+} // namespace v
+} // namespace giada
+
+#endif
diff --git a/src/gui/elems/plugin/pluginBrowser.cpp b/src/gui/elems/plugin/pluginBrowser.cpp
new file mode 100644 (file)
index 0000000..2a4d3dd
--- /dev/null
@@ -0,0 +1,121 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "gui/elems/plugin/pluginBrowser.h"
+#include "core/const.h"
+#include "core/plugins/pluginManager.h"
+#include "glue/plugin.h"
+#include "gui/elems/basics/boxtypes.h"
+#include <FL/fl_draw.H>
+
+namespace giada::v
+{
+gePluginBrowser::gePluginBrowser(int x, int y, int w, int h)
+: Fl_Browser(x, y, w, h)
+, m_widths{0}
+{
+       box(G_CUSTOM_BORDER_BOX);
+       textsize(G_GUI_FONT_SIZE_BASE);
+       textcolor(G_COLOR_LIGHT_2);
+       selection_color(G_COLOR_GREY_4);
+       color(G_COLOR_GREY_2);
+
+       this->scrollbar.color(G_COLOR_GREY_2);
+       this->scrollbar.selection_color(G_COLOR_GREY_4);
+       this->scrollbar.labelcolor(G_COLOR_LIGHT_1);
+       this->scrollbar.slider(G_CUSTOM_BORDER_BOX);
+
+       this->hscrollbar.color(G_COLOR_GREY_2);
+       this->hscrollbar.selection_color(G_COLOR_GREY_4);
+       this->hscrollbar.labelcolor(G_COLOR_LIGHT_1);
+       this->hscrollbar.slider(G_CUSTOM_BORDER_BOX);
+
+       type(FL_HOLD_BROWSER);
+
+       computeWidths();
+
+       column_widths(m_widths);
+       column_char('\t'); // tabs as column delimiters
+
+       refresh();
+
+       end();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginBrowser::refresh()
+{
+       clear();
+
+       add("NAME\tMANUFACTURER\tCATEGORY\tFORMAT\tUID");
+       add("---\t---\t---\t---\t---");
+
+       for (m::PluginManager::PluginInfo pi : c::plugin::getPluginsInfo())
+       {
+               std::string s;
+               if (pi.isKnown)
+               {
+                       std::string m = pi.exists ? "" : "@-";
+
+                       s = m + pi.name + "\t" + m + pi.manufacturerName + "\t" + m +
+                           pi.category + "\t" + m + pi.format + "\t" + m + pi.uid;
+               }
+               else
+                       std::string s = "?\t?\t?\t?\t? " + pi.uid + " ?";
+
+               add(s.c_str());
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginBrowser::computeWidths()
+{
+       int w0, w1, w3;
+       for (m::PluginManager::PluginInfo pi : c::plugin::getPluginsInfo())
+       {
+               w0 = static_cast<int>(fl_width(pi.name.c_str()));
+               w1 = static_cast<int>(fl_width(pi.manufacturerName.c_str()));
+               w3 = static_cast<int>(fl_width(pi.format.c_str()));
+               if (w0 > m_widths[0])
+                       m_widths[0] = w0;
+               if (w1 > m_widths[1])
+                       m_widths[1] = w1;
+               if (w3 > m_widths[3])
+                       m_widths[3] = w3;
+       }
+       m_widths[0] += 60;
+       m_widths[1] += 60;
+       m_widths[2] = static_cast<int>(fl_width("CATEGORY") + 60);
+       m_widths[3] += 60;
+       m_widths[4] = 0;
+}
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/plugin/pluginBrowser.h b/src/gui/elems/plugin/pluginBrowser.h
new file mode 100644 (file)
index 0000000..0b37df0
--- /dev/null
@@ -0,0 +1,52 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef GE_PLUGIN_BROWSER_H
+#define GE_PLUGIN_BROWSER_H
+
+#include <FL/Fl_Browser.H>
+
+namespace giada::v
+{
+class gePluginBrowser : public Fl_Browser
+{
+public:
+       gePluginBrowser(int x, int y, int w, int h);
+
+       void refresh();
+
+private:
+       void computeWidths();
+
+       int m_widths[5];
+};
+} // namespace giada::v
+
+#endif
+
+#endif
diff --git a/src/gui/elems/plugin/pluginElement.cpp b/src/gui/elems/plugin/pluginElement.cpp
new file mode 100644 (file)
index 0000000..34eecda
--- /dev/null
@@ -0,0 +1,190 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "pluginElement.h"
+#include "core/graphics.h"
+#include "core/plugins/plugin.h"
+#include "core/plugins/pluginHost.h"
+#include "glue/plugin.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dialogs/pluginList.h"
+#include "gui/dialogs/pluginWindow.h"
+#include "gui/dialogs/pluginWindowGUI.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/choice.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#include <cassert>
+#include <string>
+
+namespace giada::v
+{
+gePluginElement::gePluginElement(int x, int y, c::plugin::Plugin data)
+: gePack(x, y, Direction::HORIZONTAL)
+, button(0, 0, 196, G_GUI_UNIT)
+, program(0, 0, 132, G_GUI_UNIT)
+, bypass(0, 0, G_GUI_UNIT, G_GUI_UNIT)
+, shiftUp(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", fxShiftUpOff_xpm, fxShiftUpOn_xpm)
+, shiftDown(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", fxShiftDownOff_xpm, fxShiftDownOn_xpm)
+, remove(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", fxRemoveOff_xpm, fxRemoveOn_xpm)
+, m_plugin(data)
+{
+       add(&button);
+       add(&program);
+       add(&bypass);
+       add(&shiftUp);
+       add(&shiftDown);
+       add(&remove);
+
+       resizable(button);
+
+       remove.callback(cb_removePlugin, (void*)this);
+
+       if (!m_plugin.valid)
+       {
+               button.copy_label(m_plugin.uniqueId.c_str());
+               button.deactivate();
+               bypass.deactivate();
+               shiftUp.deactivate();
+               shiftDown.deactivate();
+               return;
+       }
+
+       button.copy_label(m_plugin.name.c_str());
+       button.callback(cb_openPluginWindow, (void*)this);
+
+       program.onChange = [pluginId = m_plugin.id](ID id) {
+               c::plugin::setProgram(pluginId, id);
+       };
+
+       for (const auto& p : m_plugin.programs)
+               program.addItem(u::gui::removeFltkChars(p.name));
+
+       if (program.countItems() == 0)
+       {
+               program.addItem("-- no programs --\0");
+               program.deactivate();
+       }
+       else
+               program.showItem(m_plugin.currentProgram);
+
+       bypass.callback(cb_setBypass, (void*)this);
+       bypass.type(FL_TOGGLE_BUTTON);
+       bypass.value(m_plugin.isBypassed ? 0 : 1);
+
+       shiftUp.callback(cb_shiftUp, (void*)this);
+       shiftDown.callback(cb_shiftDown, (void*)this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+ID gePluginElement::getPluginId() const
+{
+       return m_plugin.id;
+}
+
+const m::Plugin& gePluginElement::getPluginRef() const
+{
+       return m_plugin.getPluginRef();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginElement::cb_removePlugin(Fl_Widget* /*w*/, void* p) { ((gePluginElement*)p)->cb_removePlugin(); }
+void gePluginElement::cb_openPluginWindow(Fl_Widget* /*w*/, void* p) { ((gePluginElement*)p)->cb_openPluginWindow(); }
+void gePluginElement::cb_setBypass(Fl_Widget* /*w*/, void* p) { ((gePluginElement*)p)->cb_setBypass(); }
+void gePluginElement::cb_shiftUp(Fl_Widget* /*w*/, void* p) { ((gePluginElement*)p)->cb_shiftUp(); }
+void gePluginElement::cb_shiftDown(Fl_Widget* /*w*/, void* p) { ((gePluginElement*)p)->cb_shiftDown(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginElement::cb_shiftUp()
+{
+       const gdPluginList* parent = static_cast<const gdPluginList*>(window());
+
+       c::plugin::swapPlugins(m_plugin.getPluginRef(), parent->getPrevElement(*this).getPluginRef(), m_plugin.channelId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginElement::cb_shiftDown()
+{
+       const gdPluginList* parent = static_cast<const gdPluginList*>(window());
+
+       c::plugin::swapPlugins(m_plugin.getPluginRef(), parent->getNextElement(*this).getPluginRef(), m_plugin.channelId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginElement::cb_removePlugin()
+{
+       /* Any subwindow linked to the plugin must be destroyed first. The 
+       pluginWindow has id = id_plugin + 1, because id=0 is reserved for the parent 
+       window 'add plugin'.*/
+
+       static_cast<gdWindow*>(window())->delSubWindow(m_plugin.id + 1);
+       c::plugin::freePlugin(m_plugin.getPluginRef(), m_plugin.channelId);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginElement::cb_openPluginWindow()
+{
+       /* The new pluginWindow has id = id_plugin + 1, because id=0 is reserved for 
+       the parent window 'add plugin'. */
+
+       const int pwid = m_plugin.id + 1;
+
+       gdWindow* parent = static_cast<gdWindow*>(window());
+       gdWindow* child  = parent->getChild(pwid);
+
+       /* If Plug-in window is already opened, just raise it on top and quit. */
+
+       if (child != nullptr)
+       {
+               child->show();
+               return;
+       }
+
+       if (m_plugin.hasEditor)
+               child = new gdPluginWindowGUI(m_plugin);
+       else
+               child = new gdPluginWindow(m_plugin);
+       child->setId(pwid);
+       parent->addSubWindow(child);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginElement::cb_setBypass()
+{
+       c::plugin::toggleBypass(m_plugin.id);
+}
+} // namespace giada::v
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/elems/plugin/pluginElement.h b/src/gui/elems/plugin/pluginElement.h
new file mode 100644 (file)
index 0000000..9277e4d
--- /dev/null
@@ -0,0 +1,72 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef GE_PLUGIN_ELEMENT_H
+#define GE_PLUGIN_ELEMENT_H
+
+#include "glue/plugin.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/choice.h"
+#include "gui/elems/basics/pack.h"
+
+namespace giada::v
+{
+class gePluginElement : public gePack
+{
+public:
+       gePluginElement(int x, int y, c::plugin::Plugin);
+
+       ID               getPluginId() const;
+       const m::Plugin& getPluginRef() const;
+
+       geButton button;
+       geChoice program;
+       geButton bypass;
+       geButton shiftUp;
+       geButton shiftDown;
+       geButton remove;
+
+private:
+       static void cb_removePlugin(Fl_Widget* /*w*/, void* p);
+       static void cb_openPluginWindow(Fl_Widget* /*w*/, void* p);
+       static void cb_setBypass(Fl_Widget* /*w*/, void* p);
+       static void cb_shiftUp(Fl_Widget* /*w*/, void* p);
+       static void cb_shiftDown(Fl_Widget* /*w*/, void* p);
+       void        cb_removePlugin();
+       void        cb_openPluginWindow();
+       void        cb_setBypass();
+       void        cb_shiftUp();
+       void        cb_shiftDown();
+
+       c::plugin::Plugin m_plugin;
+};
+} // namespace giada::v
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/elems/plugin/pluginParameter.cpp b/src/gui/elems/plugin/pluginParameter.cpp
new file mode 100644 (file)
index 0000000..4d2c97a
--- /dev/null
@@ -0,0 +1,90 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#include "pluginParameter.h"
+#include "core/const.h"
+#include "glue/events.h"
+#include "glue/plugin.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "gui/elems/basics/slider.h"
+
+namespace giada
+{
+namespace v
+{
+gePluginParameter::gePluginParameter(int X, int Y, int W, int labelWidth, const c::plugin::Param p)
+: Fl_Group(X, Y, W, G_GUI_UNIT)
+, m_param(p)
+{
+       begin();
+
+       const int VALUE_WIDTH = 100;
+
+       m_label = new geBox(x(), y(), labelWidth, G_GUI_UNIT);
+       m_label->copy_label(m_param.name.c_str());
+
+       m_slider = new geSlider(m_label->x() + m_label->w() + G_GUI_OUTER_MARGIN, y(),
+           w() - (m_label->x() + m_label->w() + G_GUI_OUTER_MARGIN) - VALUE_WIDTH, G_GUI_UNIT);
+       m_slider->value(m_param.value);
+       m_slider->callback(cb_setValue, (void*)this);
+
+       m_value = new geBox(m_slider->x() + m_slider->w() + G_GUI_OUTER_MARGIN, y(), VALUE_WIDTH, G_GUI_UNIT);
+       m_value->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
+       m_value->box(G_CUSTOM_BORDER_BOX);
+
+       end();
+
+       resizable(m_slider);
+       update(m_param, false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginParameter::cb_setValue(Fl_Widget* /*w*/, void* p) { ((gePluginParameter*)p)->cb_setValue(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginParameter::cb_setValue()
+{
+       c::events::setPluginParameter(0, m_param.pluginId, m_param.index,
+           m_slider->value(), Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePluginParameter::update(const c::plugin::Param& p, bool changeSlider)
+{
+       m_value->copy_label(std::string(p.text + " " + p.label).c_str());
+       if (changeSlider)
+               m_slider->value(p.value);
+}
+} // namespace v
+} // namespace giada
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/elems/plugin/pluginParameter.h b/src/gui/elems/plugin/pluginParameter.h
new file mode 100644 (file)
index 0000000..1db9f93
--- /dev/null
@@ -0,0 +1,62 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+#ifndef GE_PLUGIN_PARAMETER_H
+#define GE_PLUGIN_PARAMETER_H
+
+#include "core/types.h"
+#include "glue/plugin.h"
+#include <FL/Fl_Group.H>
+
+class geSlider;
+
+namespace giada::v
+{
+class geBox;
+class gePluginParameter : public Fl_Group
+{
+public:
+       gePluginParameter(int x, int y, int w, int labelWidth, const c::plugin::Param);
+
+       void update(const c::plugin::Param& p, bool changeSlider);
+
+private:
+       static void cb_setValue(Fl_Widget* /*w*/, void* p);
+       void        cb_setValue();
+
+       const c::plugin::Param m_param;
+
+       geBox*    m_label;
+       geSlider* m_slider;
+       geBox*    m_value;
+};
+} // namespace giada::v
+
+#endif
+
+#endif // #ifdef WITH_VST
diff --git a/src/gui/elems/sampleEditor/boostTool.cpp b/src/gui/elems/sampleEditor/boostTool.cpp
new file mode 100644 (file)
index 0000000..5c8dea0
--- /dev/null
@@ -0,0 +1,107 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "boostTool.h"
+#include "core/const.h"
+#include "core/waveFx.h"
+#include "glue/channel.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/input.h"
+#include "utils/gui.h"
+#include "utils/math.h"
+#include "utils/string.h"
+#include "waveTools.h"
+#include <FL/Fl.H>
+
+namespace giada::v
+{
+geBoostTool::geBoostTool(int X, int Y)
+: Fl_Pack(X, Y, 220, G_GUI_UNIT)
+{
+       type(Fl_Pack::HORIZONTAL);
+       spacing(G_GUI_INNER_MARGIN);
+
+       begin();
+       label     = new geBox(0, 0, u::gui::getStringRect("Boost").w, G_GUI_UNIT, "Boost", FL_ALIGN_RIGHT);
+       dial      = new geDial(0, 0, G_GUI_UNIT, G_GUI_UNIT);
+       input     = new geInput(0, 0, 70, G_GUI_UNIT);
+       normalize = new geButton(0, 0, 70, G_GUI_UNIT, "Normalize");
+       end();
+
+       dial->range(1.0f, 10.0f);
+       dial->callback(cb_setBoost, (void*)this);
+       dial->when(FL_WHEN_CHANGED | FL_WHEN_RELEASE);
+
+       input->callback(cb_setBoostNum, (void*)this);
+
+       normalize->callback(cb_normalize, (void*)this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBoostTool::rebuild()
+{
+       /*
+       const m::SampleChannel* ch = static_cast<gdSampleEditor*>(window())->ch;
+
+       input->value(u::string::fToString(u::math::linearToDB(ch->getBoost()), 2).c_str());  // 2 digits
+       // A dial greater than it's max value goes crazy
+       dial->value(ch->getBoost() <= 10.0f ? ch->getBoost() : 10.0f);*/
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBoostTool::cb_setBoost(Fl_Widget* /*w*/, void* p) { ((geBoostTool*)p)->cb_setBoost(); }
+void geBoostTool::cb_setBoostNum(Fl_Widget* /*w*/, void* p) { ((geBoostTool*)p)->cb_setBoostNum(); }
+void geBoostTool::cb_normalize(Fl_Widget* /*w*/, void* p) { ((geBoostTool*)p)->cb_normalize(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geBoostTool::cb_setBoost()
+{
+       /*const m::SampleChannel* ch = static_cast<gdSampleEditor*>(window())->ch;
+
+       c::channel::setBoost(ch->id, dial->value());*/
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBoostTool::cb_setBoostNum()
+{
+       /*const m::SampleChannel* ch = static_cast<gdSampleEditor*>(window())->ch;
+
+       c::channel::setBoost(ch->id, u::math::dBtoLinear(atof(input->value())));*/
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geBoostTool::cb_normalize()
+{
+}
+} // namespace giada::v
diff --git a/src/gui/elems/sampleEditor/boostTool.h b/src/gui/elems/sampleEditor/boostTool.h
new file mode 100644 (file)
index 0000000..a01f9c3
--- /dev/null
@@ -0,0 +1,61 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_BOOST_TOOL_H
+#define GE_BOOST_TOOL_H
+
+#include <FL/Fl_Pack.H>
+
+class geInput;
+
+namespace giada::v
+{
+class geBox;
+class geDial;
+class geButton;
+class geBoostTool : public Fl_Pack
+{
+public:
+       geBoostTool(int x, int y);
+
+       void rebuild();
+
+private:
+       static void cb_setBoost(Fl_Widget* /*w*/, void* p);
+       static void cb_setBoostNum(Fl_Widget* /*w*/, void* p);
+       static void cb_normalize(Fl_Widget* /*w*/, void* p);
+       void        cb_setBoost();
+       void        cb_setBoostNum();
+       void        cb_normalize();
+
+       geBox*    label;
+       geDial*   dial;
+       geInput*  input;
+       geButton* normalize;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/sampleEditor/panTool.cpp b/src/gui/elems/sampleEditor/panTool.cpp
new file mode 100644 (file)
index 0000000..bcf685b
--- /dev/null
@@ -0,0 +1,115 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "panTool.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "core/waveFx.h"
+#include "glue/events.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "utils/gui.h"
+#include "utils/math.h"
+#include "utils/string.h"
+#include "waveTools.h"
+#include <FL/Fl.H>
+
+namespace giada
+{
+namespace v
+{
+gePanTool::gePanTool(const c::sampleEditor::Data& d, int x, int y)
+: gePack(x, y, Direction::HORIZONTAL)
+, m_data(nullptr)
+, m_label(0, 0, 60, G_GUI_UNIT, "Pan", FL_ALIGN_LEFT)
+, m_dial(0, 0, G_GUI_UNIT, G_GUI_UNIT)
+, m_input(0, 0, 70, G_GUI_UNIT)
+, m_reset(0, 0, 70, G_GUI_UNIT, "Reset")
+{
+       add(&m_label);
+       add(&m_dial);
+       add(&m_input);
+       add(&m_reset);
+
+       m_dial.range(0.0f, G_MAX_PAN);
+       m_dial.callback(cb_panning, (void*)this);
+
+       m_input.align(FL_ALIGN_RIGHT);
+       m_input.readonly(1);
+       m_input.cursor_color(FL_WHITE);
+
+       m_reset.callback(cb_panReset, (void*)this);
+
+       rebuild(d);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePanTool::rebuild(const c::sampleEditor::Data& d)
+{
+       m_data = &d;
+       update(m_data->pan);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePanTool::update(float v)
+{
+       m_dial.value(v);
+
+       if (v < 0.5f)
+       {
+               std::string tmp = u::string::iToString((int)((-v * 200.0f) + 100.0f)) + " L";
+               m_input.value(tmp.c_str());
+       }
+       else if (v == 0.5)
+               m_input.value("C");
+       else
+       {
+               std::string tmp = u::string::iToString((int)((v * 200.0f) - 100.0f)) + " R";
+               m_input.value(tmp.c_str());
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePanTool::cb_panning(Fl_Widget* /*w*/, void* p) { ((gePanTool*)p)->cb_panning(); }
+void gePanTool::cb_panReset(Fl_Widget* /*w*/, void* p) { ((gePanTool*)p)->cb_panReset(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gePanTool::cb_panning()
+{
+       c::events::sendChannelPan(m_data->channelId, m_dial.value());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePanTool::cb_panReset()
+{
+       c::events::sendChannelPan(m_data->channelId, 0.5f);
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/sampleEditor/panTool.h b/src/gui/elems/sampleEditor/panTool.h
new file mode 100644 (file)
index 0000000..5d97aa0
--- /dev/null
@@ -0,0 +1,65 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_PAN_TOOL_H
+#define GE_PAN_TOOL_H
+
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/basics/pack.h"
+
+namespace giada::c::sampleEditor
+{
+struct Data;
+}
+namespace giada::v
+{
+class gePanTool : public gePack
+{
+public:
+       gePanTool(const c::sampleEditor::Data& d, int x, int y);
+
+       void rebuild(const c::sampleEditor::Data& d);
+       void update(float v);
+
+  private:
+       static void cb_panning(Fl_Widget* /*w*/, void* p);
+       static void cb_panReset(Fl_Widget* /*w*/, void* p);
+       void        cb_panning();
+       void        cb_panReset();
+
+       const c::sampleEditor::Data* m_data;
+
+       geBox    m_label;
+       geDial   m_dial;
+       geInput  m_input;
+       geButton m_reset;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/sampleEditor/pitchTool.cpp b/src/gui/elems/sampleEditor/pitchTool.cpp
new file mode 100644 (file)
index 0000000..69cad2b
--- /dev/null
@@ -0,0 +1,159 @@
+
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "pitchTool.h"
+#include "core/const.h"
+#include "core/graphics.h"
+#include "core/model/model.h"
+#include "glue/events.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/input.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <FL/Fl.H>
+
+namespace giada::v
+{
+gePitchTool::gePitchTool(const c::sampleEditor::Data& d, int x, int y)
+: gePack(x, y, Direction::HORIZONTAL)
+, m_data(nullptr)
+, m_label(0, 0, 60, G_GUI_UNIT, "Pitch", FL_ALIGN_LEFT)
+, m_dial(0, 0, G_GUI_UNIT, G_GUI_UNIT)
+, m_input(0, 0, 70, G_GUI_UNIT)
+, m_pitchToBar(0, 0, 70, G_GUI_UNIT, "To bar")
+, m_pitchToSong(0, 0, 70, G_GUI_UNIT, "To song")
+, m_pitchHalf(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", divideOff_xpm, divideOn_xpm)
+, m_pitchDouble(0, 0, G_GUI_UNIT, G_GUI_UNIT, "", multiplyOff_xpm, multiplyOn_xpm)
+, m_pitchReset(0, 0, 70, G_GUI_UNIT, "Reset")
+{
+       add(&m_label);
+       add(&m_dial);
+       add(&m_input);
+       add(&m_pitchToBar);
+       add(&m_pitchToSong);
+       add(&m_pitchHalf);
+       add(&m_pitchDouble);
+       add(&m_pitchReset);
+
+       m_dial.range(0.01f, 4.0f);
+       m_dial.callback(cb_setPitch, (void*)this);
+       m_dial.when(FL_WHEN_RELEASE);
+
+       m_input.align(FL_ALIGN_RIGHT);
+       m_input.callback(cb_setPitchNum, (void*)this);
+       m_input.when(FL_WHEN_RELEASE | FL_WHEN_ENTER_KEY);
+
+       m_pitchToBar.callback(cb_setPitchToBar, (void*)this);
+       m_pitchToSong.callback(cb_setPitchToSong, (void*)this);
+       m_pitchHalf.callback(cb_setPitchHalf, (void*)this);
+       m_pitchDouble.callback(cb_setPitchDouble, (void*)this);
+       m_pitchReset.callback(cb_resetPitch, (void*)this);
+
+       rebuild(d);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::rebuild(const c::sampleEditor::Data& d)
+{
+       m_data = &d;
+       update(m_data->pitch, /*isDial=*/false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::update(float v, bool isDial)
+{
+       m_input.value(u::string::fToString(v, 4).c_str()); // 4 digits
+       if (!isDial)
+               m_dial.value(v);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::cb_setPitch(Fl_Widget* /*w*/, void* p) { ((gePitchTool*)p)->cb_setPitch(); }
+void gePitchTool::cb_setPitchToBar(Fl_Widget* /*w*/, void* p) { ((gePitchTool*)p)->cb_setPitchToBar(); }
+void gePitchTool::cb_setPitchToSong(Fl_Widget* /*w*/, void* p) { ((gePitchTool*)p)->cb_setPitchToSong(); }
+void gePitchTool::cb_setPitchHalf(Fl_Widget* /*w*/, void* p) { ((gePitchTool*)p)->cb_setPitchHalf(); }
+void gePitchTool::cb_setPitchDouble(Fl_Widget* /*w*/, void* p) { ((gePitchTool*)p)->cb_setPitchDouble(); }
+void gePitchTool::cb_resetPitch(Fl_Widget* /*w*/, void* p) { ((gePitchTool*)p)->cb_resetPitch(); }
+void gePitchTool::cb_setPitchNum(Fl_Widget* /*w*/, void* p) { ((gePitchTool*)p)->cb_setPitchNum(); }
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::cb_setPitch()
+{
+       c::events::setChannelPitch(m_data->channelId, m_dial.value(), Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::cb_setPitchNum()
+{
+       c::events::setChannelPitch(m_data->channelId, atof(m_input.value()), Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::cb_setPitchHalf()
+{
+       c::events::setChannelPitch(m_data->channelId, m_dial.value() / 2, Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::cb_setPitchDouble()
+{
+       c::events::setChannelPitch(m_data->channelId, m_dial.value() * 2, Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::cb_setPitchToBar()
+{
+       c::events::setChannelPitch(m_data->channelId, m_data->end / (float)m_data->getFramesInBar(),
+           Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::cb_setPitchToSong()
+{
+       c::events::setChannelPitch(m_data->channelId, m_data->end / (float)m_data->getFramesInLoop(),
+           Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void gePitchTool::cb_resetPitch()
+{
+       c::events::setChannelPitch(m_data->channelId, G_DEFAULT_PITCH, Thread::MAIN);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/sampleEditor/pitchTool.h b/src/gui/elems/sampleEditor/pitchTool.h
new file mode 100644 (file)
index 0000000..f206627
--- /dev/null
@@ -0,0 +1,80 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_PITCH_TOOL_H
+#define GE_PITCH_TOOL_H
+
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/basics/pack.h"
+
+namespace giada::c::sampleEditor
+{
+struct Data;
+}
+
+namespace giada::v
+{
+class gePitchTool : public gePack
+{
+public:
+       gePitchTool(const c::sampleEditor::Data& d, int x, int y);
+
+       void rebuild(const c::sampleEditor::Data& d);
+       void update(float v, bool isDial = false);
+
+private:
+       static void cb_setPitch(Fl_Widget* /*w*/, void* p);
+       static void cb_setPitchToBar(Fl_Widget* /*w*/, void* p);
+       static void cb_setPitchToSong(Fl_Widget* /*w*/, void* p);
+       static void cb_setPitchHalf(Fl_Widget* /*w*/, void* p);
+       static void cb_setPitchDouble(Fl_Widget* /*w*/, void* p);
+       static void cb_resetPitch(Fl_Widget* /*w*/, void* p);
+       static void cb_setPitchNum(Fl_Widget* /*w*/, void* p);
+       void        cb_setPitch();
+       void        cb_setPitchToBar();
+       void        cb_setPitchToSong();
+       void        cb_setPitchHalf();
+       void        cb_setPitchDouble();
+       void        cb_resetPitch();
+       void        cb_setPitchNum();
+
+       const c::sampleEditor::Data* m_data;
+
+       geBox    m_label;
+       geDial   m_dial;
+       geInput  m_input;
+       geButton m_pitchToBar;
+       geButton m_pitchToSong;
+       geButton m_pitchHalf;
+       geButton m_pitchDouble;
+       geButton m_pitchReset;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/sampleEditor/rangeTool.cpp b/src/gui/elems/sampleEditor/rangeTool.cpp
new file mode 100644 (file)
index 0000000..8f8e85d
--- /dev/null
@@ -0,0 +1,105 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "rangeTool.h"
+#include "core/model/model.h"
+#include "core/wave.h"
+#include "glue/channel.h"
+#include "glue/sampleEditor.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include "waveTools.h"
+#include <FL/Fl.H>
+#include <cassert>
+
+namespace giada
+{
+namespace v
+{
+geRangeTool::geRangeTool(const c::sampleEditor::Data& d, int x, int y)
+: gePack(x, y, Direction::HORIZONTAL)
+, m_data(nullptr)
+, m_label(0, 0, 60, G_GUI_UNIT, "Range", FL_ALIGN_LEFT)
+, m_begin(0, 0, 70, G_GUI_UNIT)
+, m_end(0, 0, 70, G_GUI_UNIT)
+, m_reset(0, 0, 70, G_GUI_UNIT, "Reset")
+{
+       add(&m_label);
+       add(&m_begin);
+       add(&m_end);
+       add(&m_reset);
+
+       m_begin.type(FL_INT_INPUT);
+       m_begin.when(FL_WHEN_RELEASE | FL_WHEN_ENTER_KEY); // on focus lost or enter key
+       m_begin.callback(cb_setChanPos, this);
+
+       m_end.type(FL_INT_INPUT);
+       m_end.when(FL_WHEN_RELEASE | FL_WHEN_ENTER_KEY); // on focus lost or enter key
+       m_end.callback(cb_setChanPos, this);
+
+       m_reset.callback(cb_resetStartEnd, this);
+
+       rebuild(d);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geRangeTool::rebuild(const c::sampleEditor::Data& d)
+{
+       m_data = &d;
+       update(m_data->begin, m_data->end);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geRangeTool::update(Frame begin, Frame end)
+{
+       m_begin.value(std::to_string(begin).c_str());
+       m_end.value(std::to_string(end).c_str());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geRangeTool::cb_setChanPos(Fl_Widget* /*w*/, void* p) { ((geRangeTool*)p)->cb_setChanPos(); }
+void geRangeTool::cb_resetStartEnd(Fl_Widget* /*w*/, void* p) { ((geRangeTool*)p)->cb_resetStartEnd(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geRangeTool::cb_setChanPos()
+{
+       c::sampleEditor::setBeginEnd(m_data->channelId, atoi(m_begin.value()), atoi(m_end.value()));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geRangeTool::cb_resetStartEnd()
+{
+       c::sampleEditor::setBeginEnd(m_data->channelId, 0, m_data->waveSize - 1);
+}
+
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/sampleEditor/rangeTool.h b/src/gui/elems/sampleEditor/rangeTool.h
new file mode 100644 (file)
index 0000000..ed37e5e
--- /dev/null
@@ -0,0 +1,65 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_RANGE_TOOL_H
+#define GE_RANGE_TOOL_H
+
+#include "core/types.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/basics/pack.h"
+
+namespace giada::c::sampleEditor
+{
+struct Data;
+}
+namespace giada::v
+{
+class geRangeTool : public gePack
+{
+public:
+       geRangeTool(const c::sampleEditor::Data& d, int x, int y);
+
+       void rebuild(const c::sampleEditor::Data& d);
+       void update(Frame begin, Frame end);
+
+  private:
+       static void cb_setChanPos(Fl_Widget* /*w*/, void* p);
+       static void cb_resetStartEnd(Fl_Widget* /*w*/, void* p);
+       void        cb_setChanPos();
+       void        cb_resetStartEnd();
+
+       const c::sampleEditor::Data* m_data;
+
+       geBox    m_label;
+       geInput  m_begin;
+       geInput  m_end;
+       geButton m_reset;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/sampleEditor/shiftTool.cpp b/src/gui/elems/sampleEditor/shiftTool.cpp
new file mode 100644 (file)
index 0000000..f5c56c3
--- /dev/null
@@ -0,0 +1,103 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "shiftTool.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "glue/sampleEditor.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/dialogs/warnings.h"
+#include "utils/gui.h"
+#include "utils/string.h"
+#include <cassert>
+#include <cstdlib>
+
+namespace giada
+{
+namespace v
+{
+geShiftTool::geShiftTool(const c::sampleEditor::Data& d, int x, int y)
+: gePack(x, y, Direction::HORIZONTAL)
+, m_data(nullptr)
+, m_label(0, 0, 60, G_GUI_UNIT, "Shift", FL_ALIGN_LEFT)
+, m_shift(0, 0, 70, G_GUI_UNIT)
+, m_reset(0, 0, 70, G_GUI_UNIT, "Reset")
+{
+       add(&m_label);
+       add(&m_shift);
+       add(&m_reset);
+
+       m_shift.type(FL_INT_INPUT);
+       m_shift.when(FL_WHEN_RELEASE | FL_WHEN_ENTER_KEY); // on focus lost or enter key
+       m_shift.callback(cb_setShift, (void*)this);
+
+       m_reset.callback(cb_reset, (void*)this);
+
+       rebuild(d);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geShiftTool::cb_setShift(Fl_Widget* /*w*/, void* p) { ((geShiftTool*)p)->cb_setShift(); }
+void geShiftTool::cb_reset(Fl_Widget* /*w*/, void* p) { ((geShiftTool*)p)->cb_reset(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geShiftTool::cb_setShift()
+{
+       shift(atoi(m_shift.value()));
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geShiftTool::cb_reset()
+{
+       shift(0);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geShiftTool::rebuild(const c::sampleEditor::Data& d)
+{
+       m_data = &d;
+       update(m_data->shift);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geShiftTool::update(Frame shift)
+{
+       m_shift.value(std::to_string(shift).c_str());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geShiftTool::shift(int f)
+{
+       c::sampleEditor::shift(m_data->channelId, f);
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/sampleEditor/shiftTool.h b/src/gui/elems/sampleEditor/shiftTool.h
new file mode 100644 (file)
index 0000000..09ec99e
--- /dev/null
@@ -0,0 +1,66 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SHIFT_TOOL_H
+#define GE_SHIFT_TOOL_H
+
+#include "core/types.h"
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/button.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/basics/pack.h"
+
+namespace giada::c::sampleEditor
+{
+struct Data;
+}
+namespace giada::v
+{
+class geShiftTool : public gePack
+{
+public:
+       geShiftTool(const c::sampleEditor::Data& d, int x, int y);
+
+       void rebuild(const c::sampleEditor::Data& d);
+       void update(Frame shift);
+
+  private:
+       static void cb_setShift(Fl_Widget* /*w*/, void* p);
+       static void cb_reset(Fl_Widget* /*w*/, void* p);
+       void        cb_setShift();
+       void        cb_reset();
+
+       void shift(int f);
+
+       const c::sampleEditor::Data* m_data;
+
+       geBox    m_label;
+       geInput  m_shift;
+       geButton m_reset;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/sampleEditor/volumeTool.cpp b/src/gui/elems/sampleEditor/volumeTool.cpp
new file mode 100644 (file)
index 0000000..5fd7d25
--- /dev/null
@@ -0,0 +1,103 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "volumeTool.h"
+#include "core/const.h"
+#include "glue/events.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "utils/gui.h"
+#include "utils/math.h"
+#include "utils/string.h"
+#include <FL/Fl_Pack.H>
+#include <cmath>
+#include <cstdlib>
+
+namespace giada
+{
+namespace v
+{
+geVolumeTool::geVolumeTool(const c::sampleEditor::Data& d, int x, int y)
+: gePack(x, y, Direction::HORIZONTAL)
+, m_data(nullptr)
+, m_label(0, 0, 60, G_GUI_UNIT, "Volume", FL_ALIGN_LEFT)
+, m_dial(0, 0, G_GUI_UNIT, G_GUI_UNIT)
+, m_input(0, 0, 70, G_GUI_UNIT)
+{
+       add(&m_label);
+       add(&m_dial);
+       add(&m_input);
+
+       m_dial.range(0.0f, 1.0f);
+       m_dial.callback(cb_setVolume, (void*)this);
+
+       m_input.callback(cb_setVolumeNum, (void*)this);
+
+       rebuild(d);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geVolumeTool::rebuild(const c::sampleEditor::Data& d)
+{
+       m_data = &d;
+       update(m_data->volume, /*isDial=*/false);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geVolumeTool::update(float v, bool isDial)
+{
+       std::string tmp = "-inf";
+       float       dB  = u::math::linearToDB(v);
+       if (dB > -INFINITY)
+               tmp = u::string::fToString(dB, 2); // 2 digits
+       m_input.value(tmp.c_str());
+       if (!isDial)
+               m_dial.value(v);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geVolumeTool::cb_setVolume(Fl_Widget* /*w*/, void* p) { ((geVolumeTool*)p)->cb_setVolume(); }
+void geVolumeTool::cb_setVolumeNum(Fl_Widget* /*w*/, void* p) { ((geVolumeTool*)p)->cb_setVolumeNum(); }
+
+/* -------------------------------------------------------------------------- */
+
+void geVolumeTool::cb_setVolume()
+{
+       c::events::setChannelVolume(m_data->channelId, m_dial.value(), Thread::MAIN);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geVolumeTool::cb_setVolumeNum()
+{
+       c::events::setChannelVolume(m_data->channelId, u::math::dBtoLinear(atof(m_input.value())),
+           Thread::MAIN);
+}
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/sampleEditor/volumeTool.h b/src/gui/elems/sampleEditor/volumeTool.h
new file mode 100644 (file)
index 0000000..2b75340
--- /dev/null
@@ -0,0 +1,63 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_VOLUME_TOOL_H
+#define GE_VOLUME_TOOL_H
+
+#include "gui/elems/basics/box.h"
+#include "gui/elems/basics/dial.h"
+#include "gui/elems/basics/input.h"
+#include "gui/elems/basics/pack.h"
+
+namespace giada::c::sampleEditor
+{
+struct Data;
+}
+namespace giada::v
+{
+class geVolumeTool : public gePack
+{
+public:
+       geVolumeTool(const c::sampleEditor::Data& d, int x, int y);
+
+       void rebuild(const c::sampleEditor::Data& d);
+       void update(float v, bool isDial = false);
+
+  private:
+       static void cb_setVolume(Fl_Widget* /*w*/, void* p);
+       static void cb_setVolumeNum(Fl_Widget* /*w*/, void* p);
+       void        cb_setVolume();
+       void        cb_setVolumeNum();
+
+       const c::sampleEditor::Data* m_data;
+
+       geBox   m_label;
+       geDial  m_dial;
+       geInput m_input;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/sampleEditor/waveTools.cpp b/src/gui/elems/sampleEditor/waveTools.cpp
new file mode 100644 (file)
index 0000000..e271d3c
--- /dev/null
@@ -0,0 +1,244 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "waveTools.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "core/waveFx.h"
+#include "glue/sampleEditor.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "utils/gui.h"
+#include "waveform.h"
+#include <FL/Fl_Menu_Button.H>
+#include <FL/Fl_Menu_Item.H>
+#include <cstdint>
+
+namespace giada
+{
+namespace v
+{
+namespace
+{
+enum class Menu
+{
+       CUT = 0,
+       COPY,
+       PASTE,
+       TRIM,
+       SILENCE,
+       REVERSE,
+       NORMALIZE,
+       FADE_IN,
+       FADE_OUT,
+       SMOOTH_EDGES,
+       SET_BEGIN_END,
+       TO_NEW_CHANNEL
+};
+
+/* -------------------------------------------------------------------------- */
+
+void menuCallback_(Fl_Widget* w, void* v)
+{
+       const geWaveTools* wt = static_cast<geWaveTools*>(w);
+
+       ID   channelId    = wt->getChannelData().channelId;
+       Menu selectedItem = (Menu)(intptr_t)v;
+
+       Frame a = wt->waveform->getSelectionA();
+       Frame b = wt->waveform->getSelectionB();
+
+       switch (selectedItem)
+       {
+       case Menu::CUT:
+               c::sampleEditor::cut(channelId, a, b);
+               break;
+       case Menu::COPY:
+               c::sampleEditor::copy(channelId, a, b);
+               break;
+       case Menu::PASTE:
+               c::sampleEditor::paste(channelId, a);
+               break;
+       case Menu::TRIM:
+               c::sampleEditor::trim(channelId, a, b);
+               break;
+       case Menu::SILENCE:
+               c::sampleEditor::silence(channelId, a, b);
+               break;
+       case Menu::REVERSE:
+               c::sampleEditor::reverse(channelId, a, b);
+               break;
+       case Menu::NORMALIZE:
+               c::sampleEditor::normalize(channelId, a, b);
+               break;
+       case Menu::FADE_IN:
+               c::sampleEditor::fade(channelId, a, b, m::wfx::Fade::IN);
+               break;
+       case Menu::FADE_OUT:
+               c::sampleEditor::fade(channelId, a, b, m::wfx::Fade::OUT);
+               break;
+       case Menu::SMOOTH_EDGES:
+               c::sampleEditor::smoothEdges(channelId, a, b);
+               break;
+       case Menu::SET_BEGIN_END:
+               c::sampleEditor::setBeginEnd(channelId, a, b);
+               break;
+       case Menu::TO_NEW_CHANNEL:
+               c::sampleEditor::toNewChannel(channelId, a, b);
+               break;
+       }
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geWaveTools::geWaveTools(int x, int y, int w, int h, bool gridEnabled, int gridVal)
+: Fl_Scroll(x, y, w, h, nullptr)
+, m_data(nullptr)
+{
+       type(Fl_Scroll::HORIZONTAL_ALWAYS);
+       hscrollbar.color(G_COLOR_GREY_2);
+       hscrollbar.selection_color(G_COLOR_GREY_4);
+       hscrollbar.labelcolor(G_COLOR_LIGHT_1);
+       hscrollbar.slider(G_CUSTOM_BORDER_BOX);
+
+       waveform = new v::geWaveform(x, y, w, h - 24, gridEnabled, gridVal);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveTools::rebuild(const c::sampleEditor::Data& d)
+{
+       m_data = &d;
+       waveform->rebuild(d);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveTools::refresh()
+{
+       if (m_data->a_getPreviewStatus() == ChannelStatus::PLAY)
+               waveform->redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveTools::resize(int x, int y, int w, int h)
+{
+       Fl_Widget::resize(x, y, w, h);
+
+       if (this->w() == w || (this->w() != w && this->h() != h))
+       { // vertical or both resize
+               waveform->resize(x, y, waveform->w(), h - 24);
+               waveform->rebuild(*m_data);
+       }
+
+       if (this->w() > waveform->w())
+               waveform->stretchToWindow();
+
+       int offset = waveform->x() + waveform->w() - this->w() - this->x();
+       if (offset < 0)
+               waveform->position(waveform->x() - offset, this->y());
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geWaveTools::handle(int e)
+{
+       switch (e)
+       {
+       case FL_MOUSEWHEEL:
+       {
+               waveform->setZoom(Fl::event_dy() == 1 ? geWaveform::Zoom::OUT : geWaveform::Zoom::IN);
+               redraw();
+               return 1;
+       }
+       case FL_PUSH:
+       {
+               if (Fl::event_button3()) // right button
+               {
+                       openMenu();
+                       return 1;
+               }
+               Fl::focus(waveform);
+               return Fl_Group::handle(e);
+       }
+       default:
+               return Fl_Group::handle(e);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveTools::openMenu()
+{
+       Fl_Menu_Item menu[] = {
+           u::gui::makeMenuItem("Cut", menuCallback_, (void*)Menu::CUT),
+           u::gui::makeMenuItem("Copy", menuCallback_, (void*)Menu::COPY),
+           u::gui::makeMenuItem("Paste", menuCallback_, (void*)Menu::PASTE),
+           u::gui::makeMenuItem("Trim", menuCallback_, (void*)Menu::TRIM),
+           u::gui::makeMenuItem("Silence", menuCallback_, (void*)Menu::SILENCE),
+           u::gui::makeMenuItem("Reverse", menuCallback_, (void*)Menu::REVERSE),
+           u::gui::makeMenuItem("Normalize", menuCallback_, (void*)Menu::NORMALIZE),
+           u::gui::makeMenuItem("Fade in", menuCallback_, (void*)Menu::FADE_IN),
+           u::gui::makeMenuItem("Fade out", menuCallback_, (void*)Menu::FADE_OUT),
+           u::gui::makeMenuItem("Smooth edges", menuCallback_, (void*)Menu::SMOOTH_EDGES),
+           u::gui::makeMenuItem("Set begin/end here", menuCallback_, (void*)Menu::SET_BEGIN_END),
+           u::gui::makeMenuItem("Copy to new channel", menuCallback_, (void*)Menu::TO_NEW_CHANNEL),
+           {}};
+
+       if (!waveform->isSelected())
+       {
+               menu[(int)Menu::CUT].deactivate();
+               menu[(int)Menu::COPY].deactivate();
+               menu[(int)Menu::TRIM].deactivate();
+               menu[(int)Menu::SILENCE].deactivate();
+               menu[(int)Menu::REVERSE].deactivate();
+               menu[(int)Menu::NORMALIZE].deactivate();
+               menu[(int)Menu::FADE_IN].deactivate();
+               menu[(int)Menu::FADE_OUT].deactivate();
+               menu[(int)Menu::SMOOTH_EDGES].deactivate();
+               menu[(int)Menu::SET_BEGIN_END].deactivate();
+               menu[(int)Menu::TO_NEW_CHANNEL].deactivate();
+       }
+
+       Fl_Menu_Button b(0, 0, 100, 50);
+       b.box(G_CUSTOM_BORDER_BOX);
+       b.textsize(G_GUI_FONT_SIZE_BASE);
+       b.textcolor(G_COLOR_LIGHT_2);
+       b.color(G_COLOR_GREY_2);
+
+       const Fl_Menu_Item* m = menu->popup(Fl::event_x(), Fl::event_y(), 0, 0, &b);
+       if (m != nullptr)
+               m->do_callback(this, m->user_data());
+
+       return;
+}
+
+} // namespace v
+} // namespace giada
diff --git a/src/gui/elems/sampleEditor/waveTools.h b/src/gui/elems/sampleEditor/waveTools.h
new file mode 100644 (file)
index 0000000..76481da
--- /dev/null
@@ -0,0 +1,72 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_WAVE_TOOLS_H
+#define GE_WAVE_TOOLS_H
+
+#include <FL/Fl_Scroll.H>
+
+namespace giada::c::sampleEditor
+{
+struct Data;
+}
+
+namespace giada::v
+{
+class geWaveform;
+class geWaveTools : public Fl_Scroll
+{
+public:
+       geWaveTools(int x, int y, int w, int h, bool gridEnabled, int gridVal);
+
+       void resize(int x, int y, int w, int h) override;
+       int  handle(int e) override;
+
+       /* rebuild
+       Updates the waveform by realloc-ing new data (i.e. when the waveform has
+       changed). */
+
+       void rebuild(const c::sampleEditor::Data& d);
+
+       /* refresh
+       Redraws the waveform, called by the video thread. This is meant to be called
+       repeatedly when you need to update the play head inside the waveform. The
+       method is smart enough to skip painting if the channel is stopped. */
+
+       void refresh();
+
+       const c::sampleEditor::Data& getChannelData() const { return *m_data; }
+
+       v::geWaveform* waveform;
+
+private:
+       void openMenu();
+
+       const c::sampleEditor::Data* m_data;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/sampleEditor/waveform.cpp b/src/gui/elems/sampleEditor/waveform.cpp
new file mode 100644 (file)
index 0000000..761434e
--- /dev/null
@@ -0,0 +1,683 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/elems/sampleEditor/waveform.h"
+#include "core/const.h"
+#include "core/mixer.h"
+#include "core/model/model.h"
+#include "core/wave.h"
+#include "core/waveFx.h"
+#include "glue/channel.h"
+#include "glue/sampleEditor.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/elems/basics/boxtypes.h"
+#include "utils/log.h"
+#include "waveTools.h"
+#include <FL/Fl_Menu_Button.H>
+#include <FL/fl_draw.H>
+#include <cassert>
+#include <cmath>
+
+namespace giada::v
+{
+geWaveform::geWaveform(int x, int y, int w, int h, bool gridEnabled, int gridVal)
+: Fl_Widget(x, y, w, h, nullptr)
+, m_selection{}
+, m_data(nullptr)
+, m_chanStart(0)
+, m_chanStartLit(false)
+, m_chanEnd(0)
+, m_chanEndLit(false)
+, m_pushed(false)
+, m_dragged(false)
+, m_resizedA(false)
+, m_resizedB(false)
+, m_ratio(0.0f)
+{
+       m_waveform.size = w;
+
+       m_grid.snap  = gridEnabled;
+       m_grid.level = gridVal;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::clearData()
+{
+       m_waveform.sup.clear();
+       m_waveform.inf.clear();
+       m_waveform.size = 0;
+       m_grid.points.clear();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geWaveform::alloc(int datasize, bool force)
+{
+       const m::Wave& wave = m_data->getWaveRef();
+
+       m_ratio = wave.getBuffer().countFrames() / (float)datasize;
+
+       /* Limit 1:1 drawing (to avoid sub-frame drawing) by keeping m_ratio >= 1. */
+
+       if (m_ratio < 1)
+       {
+               datasize = wave.getBuffer().countFrames();
+               m_ratio  = 1;
+       }
+
+       if (datasize == m_waveform.size && !force)
+               return 0;
+
+       clearData();
+
+       m_waveform.size = datasize;
+       m_waveform.sup.resize(m_waveform.size);
+       m_waveform.inf.resize(m_waveform.size);
+
+       u::log::print("[geWaveform::alloc] %d pixels, %f m_ratio\n", m_waveform.size, m_ratio);
+
+       int offset = h() / 2;
+       int zero   = y() + offset; // center, zero amplitude (-inf dB)
+
+       /* Frid frequency: store a grid point every 'gridFreq' frame (if grid is
+       enabled). TODO - this will cause round off errors, since gridFreq is integer. */
+
+       int gridFreq = m_grid.level != 0 ? wave.getBuffer().countFrames() / m_grid.level : 0;
+
+       /* Resampling the waveform, hardcore way. Many thanks to 
+       http://fourier.eng.hmc.edu/e161/lectures/resize/node3.html */
+
+       for (int i = 0; i < m_waveform.size; i++)
+       {
+
+               /* Scan the original waveform in chunks [pc, pn]. */
+
+               int pc = i * m_ratio;       // current point TODO - int until we switch to uint32_t for Wave size...
+               int pn = (i + 1) * m_ratio; // next point    TODO - int until we switch to uint32_t for Wave size...
+
+               float peaksup = 0.0f;
+               float peakinf = 0.0f;
+
+               for (int k = pc; k < pn; k++)
+               { // TODO - int until we switch to uint32_t for Wave size...
+
+                       if (k >= wave.getBuffer().countFrames())
+                               continue;
+
+                       /* Compute average of stereo signal. */
+
+                       float  avg   = 0.0f;
+                       float* frame = wave.getBuffer()[k];
+                       for (int j = 0; j < wave.getBuffer().countChannels(); j++)
+                               avg += frame[j];
+                       avg /= wave.getBuffer().countChannels();
+
+                       /* Find peaks (greater and lower). */
+
+                       if (avg > peaksup)
+                               peaksup = avg;
+                       else if (avg <= peakinf)
+                               peakinf = avg;
+
+                       /* Fill up grid vector. */
+
+                       if (gridFreq != 0 && (int)k % gridFreq == 0 && k != 0)
+                               m_grid.points.push_back(k);
+               }
+
+               m_waveform.sup[i] = zero - (peaksup * offset);
+               m_waveform.inf[i] = zero - (peakinf * offset);
+
+               // avoid window overflow
+
+               if (m_waveform.sup[i] < y())
+                       m_waveform.sup[i] = y();
+               if (m_waveform.inf[i] > y() + h() - 1)
+                       m_waveform.inf[i] = y() + h() - 1;
+       }
+
+       recalcPoints();
+       return 1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::recalcPoints()
+{
+       m_chanStart = m_data->begin;
+       m_chanEnd   = m_data->end;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::drawSelection()
+{
+       if (!isSelected())
+               return;
+
+       int a = frameToPixel(m_selection.a) + x();
+       int b = frameToPixel(m_selection.b) + x();
+
+       if (a < 0)
+               a = 0;
+       if (b >= w() + BORDER)
+               b = w() + BORDER;
+
+       if (a < b)
+               fl_rectf(a, y(), b - a, h(), G_COLOR_GREY_4);
+       else
+               fl_rectf(b, y(), a - b, h(), G_COLOR_GREY_4);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::drawWaveform(int from, int to)
+{
+       int zero = y() + (h() / 2); // zero amplitude (-inf dB)
+
+       fl_color(G_COLOR_BLACK);
+       for (int i = from; i < to; i++)
+       {
+               if (i >= m_waveform.size)
+                       break;
+               fl_line(i + x(), zero, i + x(), m_waveform.sup[i]);
+               fl_line(i + x(), zero, i + x(), m_waveform.inf[i]);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::drawGrid(int from, int to)
+{
+       fl_color(G_COLOR_GREY_3);
+       fl_line_style(FL_DASH, 1, nullptr);
+
+       for (int pf : m_grid.points)
+       {
+               int pp = frameToPixel(pf);
+               if (pp > from && pp < to)
+                       fl_line(pp + x(), y(), pp + x(), y() + h());
+       }
+
+       fl_line_style(FL_SOLID, 0, nullptr);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::drawStartEndPoints()
+{
+       /* print m_chanStart */
+
+       int lineX = frameToPixel(m_chanStart) + x();
+
+       if (m_chanStartLit)
+               fl_color(G_COLOR_LIGHT_2);
+       else
+               fl_color(G_COLOR_LIGHT_1);
+
+       /* vertical line */
+
+       fl_line(lineX, y() + 1, lineX, y() + h() - 2);
+
+       /* print flag and avoid overflow */
+
+       if (lineX + FLAG_WIDTH > w() + x() - 2)
+               fl_rectf(lineX, y() + h() - FLAG_HEIGHT - 1, w() - lineX + x() - 1, FLAG_HEIGHT);
+       else
+               fl_rectf(lineX, y() + h() - FLAG_HEIGHT - 1, FLAG_WIDTH, FLAG_HEIGHT);
+
+       /* print m_chanEnd */
+
+       lineX = frameToPixel(m_chanEnd) + x() - 1;
+       if (m_chanEndLit)
+               fl_color(G_COLOR_LIGHT_2);
+       else
+               fl_color(G_COLOR_LIGHT_1);
+
+       /* vertical line */
+
+       fl_line(lineX, y() + 1, lineX, y() + h() - 2);
+
+       if (lineX - FLAG_WIDTH < x())
+               fl_rectf(x() + 1, y() + 1, lineX - x(), FLAG_HEIGHT);
+       else
+               fl_rectf(lineX - FLAG_WIDTH, y() + 1, FLAG_WIDTH, FLAG_HEIGHT);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::drawPlayHead()
+{
+       int p = frameToPixel(m_data->a_getPreviewTracker()) + x();
+       fl_color(G_COLOR_LIGHT_2);
+       fl_line(p, y() + 1, p, y() + h() - 2);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::draw()
+{
+       assert(m_waveform.sup.size() > 0);
+       assert(m_waveform.inf.size() > 0);
+
+       fl_rectf(x(), y(), w(), h(), G_COLOR_GREY_2); // blank canvas
+
+       /* Draw things from 'from' (offset driven by the scrollbar) to 'to' (width of 
+       parent window). We don't draw the entire waveform, only the visible part. */
+
+       int from = abs(x() - parent()->x());
+       int to   = from + parent()->w();
+       if (x() + w() < parent()->w())
+               to = x() + w() - BORDER;
+
+       drawSelection();
+       drawWaveform(from, to);
+       drawGrid(from, to);
+       drawPlayHead();
+
+       fl_rect(x(), y(), w(), h(), G_COLOR_GREY_4); // border box
+
+       drawStartEndPoints();
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geWaveform::handle(int e)
+{
+       const m::Wave& wave = m_data->getWaveRef();
+
+       m_mouseX = pixelToFrame(Fl::event_x() - x());
+       m_mouseY = pixelToFrame(Fl::event_y() - y());
+
+       switch (e)
+       {
+
+       case FL_KEYDOWN:
+       {
+               if (Fl::event_key() == ' ')
+                       static_cast<v::gdSampleEditor*>(window())->cb_togglePreview();
+               else if (Fl::event_key() == FL_BackSpace)
+                       c::sampleEditor::setPreviewTracker(m_data->begin);
+               return 1;
+       }
+
+       case FL_PUSH:
+       {
+
+               if (Fl::event_clicks() > 0)
+               {
+                       selectAll();
+                       return 1;
+               }
+
+               m_pushed = true;
+
+               if (!mouseOnEnd() && !mouseOnStart())
+               {
+                       if (Fl::event_button3()) // let the parent (waveTools) handle this
+                               return 0;
+                       if (mouseOnSelectionA())
+                               m_resizedA = true;
+                       else if (mouseOnSelectionB())
+                               m_resizedB = true;
+                       else
+                       {
+                               m_dragged     = true;
+                               m_selection.a = m_mouseX;
+                               m_selection.b = m_mouseX;
+                       }
+               }
+               return 1;
+       }
+
+       case FL_RELEASE:
+       {
+
+               c::sampleEditor::setPreviewTracker(m_mouseX);
+
+               /* If selection has been done (m_dragged or resized), make sure that point A 
+                       is always lower than B. */
+
+               if (m_dragged || m_resizedA || m_resizedB)
+                       fixSelection();
+
+               /* Handle begin/end markers interaction. */
+
+               if (m_chanStartLit || m_chanEndLit)
+                       c::sampleEditor::setBeginEnd(m_data->channelId, m_chanStart, m_chanEnd);
+
+               m_pushed   = false;
+               m_dragged  = false;
+               m_resizedA = false;
+               m_resizedB = false;
+
+               redraw();
+               return 1;
+       }
+
+       case FL_ENTER:
+       { // enables FL_DRAG
+               return 1;
+       }
+
+       case FL_LEAVE:
+       {
+               if (m_chanStartLit || m_chanEndLit)
+               {
+                       m_chanStartLit = false;
+                       m_chanEndLit   = false;
+                       redraw();
+               }
+               return 1;
+       }
+
+       case FL_MOVE:
+       {
+
+               if (mouseOnStart())
+               {
+                       m_chanStartLit = true;
+                       redraw();
+               }
+               else if (m_chanStartLit)
+               {
+                       m_chanStartLit = false;
+                       redraw();
+               }
+
+               if (mouseOnEnd())
+               {
+                       m_chanEndLit = true;
+                       redraw();
+               }
+               else if (m_chanEndLit)
+               {
+                       m_chanEndLit = false;
+                       redraw();
+               }
+
+               if (mouseOnSelectionA() && isSelected())
+                       fl_cursor(FL_CURSOR_WE, FL_WHITE, FL_BLACK);
+               else if (mouseOnSelectionB() && isSelected())
+                       fl_cursor(FL_CURSOR_WE, FL_WHITE, FL_BLACK);
+               else
+                       fl_cursor(FL_CURSOR_DEFAULT, FL_WHITE, FL_BLACK);
+
+               return 1;
+       }
+
+       case FL_DRAG:
+       {
+
+               /* here the mouse is on the m_chanStart tool */
+
+               if (m_chanStartLit && m_pushed)
+               {
+                       m_chanStart = snap(m_mouseX);
+
+                       if (m_chanStart < 0)
+                               m_chanStart = 0;
+                       else if (m_chanStart >= m_chanEnd)
+                               m_chanStart = m_chanEnd - 2;
+
+                       redraw();
+               }
+               else if (m_chanEndLit && m_pushed)
+               {
+
+                       m_chanEnd = snap(m_mouseX);
+
+                       if (m_chanEnd > wave.getBuffer().countFrames())
+                               m_chanEnd = wave.getBuffer().countFrames();
+                       else if (m_chanEnd <= m_chanStart)
+                               m_chanEnd = m_chanStart + 2;
+
+                       redraw();
+               }
+
+               /* Here the mouse is on the waveform, i.e. a new selection has started. */
+
+               else if (m_dragged)
+               {
+                       m_selection.b = snap(m_mouseX);
+                       redraw();
+               }
+
+               /* here the mouse is on a selection boundary i.e. resize */
+
+               else if (m_resizedA || m_resizedB)
+               {
+                       int pos                    = snap(m_mouseX);
+                       m_resizedA ? m_selection.a = pos : m_selection.b = pos;
+                       redraw();
+               }
+
+               return 1;
+       }
+
+       default:
+               return Fl_Widget::handle(e);
+       }
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geWaveform::snap(int pos)
+{
+       // TODO use math::quantize
+       if (!m_grid.snap)
+               return pos;
+       for (int pf : m_grid.points)
+       {
+               if (pos >= pf - pixelToFrame(SNAPPING) &&
+                   pos <= pf + pixelToFrame(SNAPPING))
+               {
+                       return pf;
+               }
+       }
+       return pos;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geWaveform::mouseOnStart() const
+{
+       int mouseXp    = frameToPixel(m_mouseX);
+       int mouseYp    = frameToPixel(m_mouseY);
+       int chanStartP = frameToPixel(m_chanStart);
+       return mouseXp - (FLAG_WIDTH / 2) > chanStartP - BORDER &&
+              mouseXp - (FLAG_WIDTH / 2) <= chanStartP - BORDER + FLAG_WIDTH &&
+              mouseYp > h() - FLAG_HEIGHT;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geWaveform::mouseOnEnd() const
+{
+       int mouseXp  = frameToPixel(m_mouseX);
+       int mouseYp  = frameToPixel(m_mouseY);
+       int chanEndP = frameToPixel(m_chanEnd);
+       return mouseXp - (FLAG_WIDTH / 2) >= chanEndP - BORDER - FLAG_WIDTH &&
+              mouseXp - (FLAG_WIDTH / 2) <= chanEndP - BORDER &&
+              mouseYp <= FLAG_HEIGHT + 1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geWaveform::mouseOnSelectionA() const
+{
+       int mouseXp = frameToPixel(m_mouseX);
+       int selAp   = frameToPixel(m_selection.a);
+       return mouseXp >= selAp - (FLAG_WIDTH / 2) && mouseXp <= selAp + (FLAG_WIDTH / 2);
+}
+
+bool geWaveform::mouseOnSelectionB() const
+{
+       int mouseXp = frameToPixel(m_mouseX);
+       int selBp   = frameToPixel(m_selection.b);
+       return mouseXp >= selBp - (FLAG_WIDTH / 2) && mouseXp <= selBp + (FLAG_WIDTH / 2);
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geWaveform::pixelToFrame(int p) const
+{
+       if (p <= 0)
+               return 0;
+       if (p > m_waveform.size)
+               return m_data->waveSize - 1;
+       return p * m_ratio;
+}
+
+/* -------------------------------------------------------------------------- */
+
+int geWaveform::frameToPixel(int p) const
+{
+       return ceil(p / m_ratio);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::fixSelection()
+{
+       if (m_selection.a > m_selection.b) // inverted m_selection
+               std::swap(m_selection.a, m_selection.b);
+
+       c::sampleEditor::setPreviewTracker(m_selection.a);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::clearSelection()
+{
+       m_selection.a = 0;
+       m_selection.b = 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef G_OS_WINDOWS
+#undef IN
+#endif
+
+void geWaveform::setZoom(Zoom z)
+{
+       if (!alloc(z == Zoom::IN ? m_waveform.size * G_GUI_ZOOM_FACTOR : m_waveform.size / G_GUI_ZOOM_FACTOR))
+               return;
+
+       size(m_waveform.size, h());
+
+       /* Zoom to cursor. */
+
+       int newX = -frameToPixel(m_mouseX) + Fl::event_x();
+       if (newX > BORDER)
+               newX = BORDER;
+       position(newX, y());
+
+       /* Avoid overflow when zooming out with scrollbar like that:
+
+               |----------[scrollbar]|
+
+       Offset vs smaller:
+       
+               |[wave------------| offset > 0  smaller = false
+               |[wave----]       | offset < 0, smaller = true
+               |-------------]   | offset < 0, smaller = false  */
+
+       int parentW = parent()->w();
+       int thisW   = x() + w() - BORDER; // visible width, not full width
+
+       if (thisW < parentW)
+               position(x() + parentW - thisW, y());
+       if (smaller())
+               stretchToWindow();
+
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::stretchToWindow()
+{
+       int s = parent()->w();
+       alloc(s);
+       position(BORDER, y());
+       size(s, h());
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::rebuild(const c::sampleEditor::Data& d)
+{
+       m_data = &d;
+       clearSelection();
+       alloc(m_waveform.size, /*force=*/true);
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geWaveform::smaller() const
+{
+       return w() < parent()->w();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::setGridLevel(int l)
+{
+       m_grid.points.clear();
+       m_grid.level = l;
+       alloc(m_waveform.size, true); // force alloc
+       redraw();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool geWaveform::isSelected() const
+{
+       return m_selection.a != m_selection.b;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geWaveform::setSnap(bool v) { m_grid.snap = v; }
+bool geWaveform::getSnap() const { return m_grid.snap; }
+int  geWaveform::getSize() const { return m_waveform.size; }
+
+/* -------------------------------------------------------------------------- */
+
+int geWaveform::getSelectionA() const { return m_selection.a; }
+int geWaveform::getSelectionB() const { return m_selection.b; }
+
+void geWaveform::selectAll()
+{
+       m_selection.a = 0;
+       m_selection.b = m_data->waveSize - 1;
+       redraw();
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/sampleEditor/waveform.h b/src/gui/elems/sampleEditor/waveform.h
new file mode 100644 (file)
index 0000000..0d68338
--- /dev/null
@@ -0,0 +1,211 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_WAVEFORM_H
+#define GE_WAVEFORM_H
+
+#include "core/const.h"
+#include "core/types.h"
+#include <FL/Fl_Widget.H>
+#include <vector>
+
+namespace giada::c::sampleEditor
+{
+struct Data;
+}
+
+namespace giada::v
+{
+class geWaveform : public Fl_Widget
+{
+public:
+#ifdef G_OS_WINDOWS
+/* Fuck... */
+#undef IN
+#undef OUT
+#endif
+       enum class Zoom
+       {
+               IN,
+               OUT
+       };
+
+       geWaveform(int x, int y, int w, int h, bool gridEnabled, int gridVal);
+
+       void draw() override;
+       int  handle(int e) override;
+
+       /* isSelected
+       Tells whether a portion of the waveform has been selected. */
+
+       bool isSelected() const;
+
+       int getSelectionA() const;
+       int getSelectionB() const;
+
+       bool getSnap() const;
+       int  getSize() const;
+
+       /* recalcPoints
+       Recomputes m_chanStart, m_chanEnd, ... */
+
+       void recalcPoints();
+
+       /* zoom
+       Type == 1 : zoom out, type == -1: zoom in */
+
+       void setZoom(Zoom z);
+
+       /* strecthToWindow
+       Shrinks or enlarge the waveform to match parent's width (gWaveTools) */
+
+       void stretchToWindow();
+
+       /* rebuild
+       Redraws the waveform. */
+
+       void rebuild(const c::sampleEditor::Data& d);
+
+       /* setGridLevel
+       Sets a new frequency level for the grid. 0 means disabled. */
+
+       void setGridLevel(int l);
+
+       void setSnap(bool v);
+
+       /* clearSelection
+       Removes any active selection. */
+
+       void clearSelection();
+
+       /* setWaveId
+       Call this when the Wave ID has changed (e.g. after a reload). */
+
+       void setWaveId(ID /*id*/){/* TODO m_waveId = id;*/};
+
+private:
+       static const int FLAG_WIDTH  = 20;
+       static const int FLAG_HEIGHT = 20;
+       static const int BORDER      = 8; // window border <-> widget border
+       static const int SNAPPING    = 16;
+
+       /* selection
+       Portion of the selected wave, in frames. */
+
+       struct
+       {
+               int a;
+               int b;
+       } m_selection;
+
+       /* data
+       Real graphic stuff from the underlying waveform. */
+
+       struct
+       {
+               std::vector<int> sup;  // upper part of the waveform
+               std::vector<int> inf;  // lower part of the waveform
+               int              size; // width of the waveform to draw (in pixel)
+       } m_waveform;
+
+       struct
+       {
+               bool             snap;
+               int              level;
+               std::vector<int> points;
+       } m_grid;
+
+       /* mouseOnStart/end
+       Is mouse on start or end flag? */
+
+       bool mouseOnStart() const;
+       bool mouseOnEnd() const;
+
+       /* mouseOnSelectionA/B
+       As above, for the selection. */
+
+       bool mouseOnSelectionA() const;
+       bool mouseOnSelectionB() const;
+
+       /* smaller
+       Is the waveform smaller than the parent window? */
+
+       bool smaller() const;
+
+       int pixelToFrame(int p) const; // TODO - move these to utils::, will be needed in actionEditor
+       int frameToPixel(int f) const; // TODO - move these to utils::, will be needed in actionEditor
+
+       /* fixSelection
+       Helper function which flattens the selection if it was made from right to left 
+       (inverse selection). It also computes the absolute points. Call this one
+       whenever the selection gesture is done. */
+
+       void fixSelection();
+
+       /* clearData
+       Destroys any graphical buffer. */
+
+       void clearData();
+
+       /* snap
+       Snaps a point at 'pos' pixel. */
+
+       int snap(int pos);
+
+       /* draw*
+       Drawing functions. */
+
+       void drawSelection();
+       void drawWaveform(int from, int to);
+       void drawGrid(int from, int to);
+       void drawStartEndPoints();
+       void drawPlayHead();
+
+       void selectAll();
+
+       /* alloc
+       Allocates memory for the picture. It's smart enough not to reallocate if 
+       datasize hasn't changed, but it can be forced otherwise. */
+
+       int alloc(int datasize, bool force = false);
+
+       const c::sampleEditor::Data* m_data;
+
+       int   m_chanStart;
+       bool  m_chanStartLit;
+       int   m_chanEnd;
+       bool  m_chanEndLit;
+       bool  m_pushed;
+       bool  m_dragged;
+       bool  m_resizedA;
+       bool  m_resizedB;
+       float m_ratio;
+       int   m_mouseX;
+       int   m_mouseY;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/elems/soundMeter.cpp b/src/gui/elems/soundMeter.cpp
new file mode 100644 (file)
index 0000000..816ce9f
--- /dev/null
@@ -0,0 +1,103 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "soundMeter.h"
+#include "core/const.h"
+#include "core/kernelAudio.h"
+#include "core/types.h"
+#include "gui/drawing.h"
+#include "utils/math.h"
+#include <FL/fl_draw.H>
+#include <algorithm>
+#include <cmath>
+
+namespace giada::v
+{
+namespace
+{
+Pixel dbToPx_(float db, Pixel max)
+{
+       const float maxf = max;
+       return std::clamp(u::math::map(db, -G_MIN_DB_SCALE, 0.0f, 0.0f, maxf), 0.0f, maxf);
+}
+} // namespace
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+float geSoundMeter::Meter::compute(float peak)
+{
+       /*  dBFS (full scale) calculation, plus decay of -2dB per call. */
+
+       float dbLevelCur = u::math::linearToDB(std::fabs(peak));
+
+       if (dbLevelCur < m_dbLevelOld && m_dbLevelOld > -G_MIN_DB_SCALE)
+               dbLevelCur = m_dbLevelOld - 2.0f;
+
+       m_dbLevelOld = dbLevelCur;
+
+       return dbLevelCur;
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+geSoundMeter::geSoundMeter(int x, int y, int w, int h, const char* l)
+: Fl_Box(x, y, w, h, l)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void geSoundMeter::draw()
+{
+       const geompp::Rect outline(x(), y(), w(), h());
+       const geompp::Rect body(outline.reduced(1));
+
+       drawRect(outline, G_COLOR_GREY_4);
+
+       if (!ready)
+       {
+               drawRectf(body, G_COLOR_BLUE);
+               return;
+       }
+
+       drawRectf(body, G_COLOR_GREY_2); // Cleanup
+
+       const float dbL    = m_left.compute(peak.left);
+       const float dbR    = m_right.compute(peak.right);
+       const int   colorL = std::fabs(peak.left) > 1.0f ? G_COLOR_BLUE : G_COLOR_GREY_4;
+       const int   colorR = std::fabs(peak.right) > 1.0f ? G_COLOR_BLUE : G_COLOR_GREY_4;
+
+       const geompp::Rect bodyL(body.withTrimmedBottom(h() / 2));
+       const geompp::Rect bodyR(body.withTrimmedTop(h() / 2));
+
+       drawRectf(bodyL.withW(dbToPx_(dbL, w() - 2)), colorL);
+       drawRectf(bodyR.withW(dbToPx_(dbR, w() - 2)), colorR);
+}
+} // namespace giada::v
\ No newline at end of file
diff --git a/src/gui/elems/soundMeter.h b/src/gui/elems/soundMeter.h
new file mode 100644 (file)
index 0000000..0925e3f
--- /dev/null
@@ -0,0 +1,60 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef GE_SOUND_METER_H
+#define GE_SOUND_METER_H
+
+#include "core/types.h"
+#include <FL/Fl_Box.H>
+
+namespace giada::v
+{
+class geSoundMeter : public Fl_Box
+{
+public:
+       geSoundMeter(int x, int y, int w, int h, const char* l = 0);
+
+       void draw() override;
+
+       Peak peak;  // Peak from Mixer
+       bool ready; // Kernel state
+
+private:
+       class Meter
+       {
+       public:
+               float compute(float peak);
+
+       private:
+               float m_dbLevelOld = 0.0f;
+       };
+
+       Meter m_left;
+       Meter m_right;
+};
+} // namespace giada::v
+
+#endif
diff --git a/src/gui/types.h b/src/gui/types.h
new file mode 100644 (file)
index 0000000..97dee84
--- /dev/null
@@ -0,0 +1,39 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_V_TYPES_H
+#define G_V_TYPES_H
+
+namespace giada::v
+{
+enum class Direction
+{
+       HORIZONTAL,
+       VERTICAL
+};
+} // namespace giada::v
+
+#endif
\ No newline at end of file
diff --git a/src/gui/ui.cpp b/src/gui/ui.cpp
new file mode 100644 (file)
index 0000000..1b1e732
--- /dev/null
@@ -0,0 +1,261 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/ui.h"
+#include "core/const.h"
+#include "core/engine.h"
+#include "core/recorder.h"
+#include "gui/elems/mainWindow/keyboard/column.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/mainWindow/mainIO.h"
+#include "gui/elems/mainWindow/mainTimer.h"
+#include "gui/updater.h"
+#include "utils/gui.h"
+#include "utils/log.h"
+#ifdef WITH_VST
+#include <FL/Fl.H>
+#if defined(G_OS_LINUX) || defined(G_OS_FREEBSD)
+#include <X11/Xlib.h> // For XInitThreads
+#endif
+#endif
+
+namespace giada::v
+{
+Ui::Ui(m::Recorder& recorder, const m::Conf::Data& conf)
+: dispatcher(conf.keyBindings)
+, m_updater(*this)
+, m_blinker(0)
+{
+       dispatcher.onEventOccured = [&recorder]() {
+               recorder.startActionRecOnCallback();
+       };
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool Ui::shouldBlink() const
+{
+       return m_blinker > BLINK_RATE / 2;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::load(const m::Patch::Data& patch)
+{
+       reset();
+       mainWindow->keyboard->layout.clear();
+       for (const m::Patch::Column& col : patch.columns)
+               mainWindow->keyboard->layout.push_back({col.id, col.width});
+       mainWindow->keyboard->rebuild();
+       setMainWindowTitle(patch.name);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::store(const std::string patchName, m::Patch::Data& patch)
+{
+       patch.columns.clear();
+       mainWindow->keyboard->forEachColumn([&](const geColumn& c) {
+               patch.columns.push_back({c.id, c.w()});
+       });
+       setMainWindowTitle(patchName);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::init(int argc, char** argv, m::Engine& engine)
+{
+       /* This is of paramount importance on Linux with VST enabled, otherwise many
+       plug-ins go nuts and crash hard. It seems that some plug-ins on our Juce-based
+       PluginHost use Xlib concurrently. */
+
+#if (defined(G_OS_LINUX) || defined(G_OS_FREEBSD)) && defined(WITH_VST)
+       XInitThreads();
+#endif
+
+       mainWindow = std::make_unique<gdMainWindow>(G_MIN_GUI_WIDTH, G_MIN_GUI_HEIGHT, "", argc, argv, engine.conf.data);
+       mainWindow->resize(engine.conf.data.mainWindowX, engine.conf.data.mainWindowY, engine.conf.data.mainWindowW,
+           engine.conf.data.mainWindowH);
+
+       setMainWindowTitle(engine.patch.data.name == "" ? G_DEFAULT_PATCH_NAME : engine.patch.data.name);
+
+       m_updater.init(engine.model);
+
+       if (engine.kernelAudio.isReady())
+               rebuildStaticWidgets();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::reset()
+{
+       setMainWindowTitle(G_DEFAULT_PATCH_NAME);
+       rebuildStaticWidgets();
+       closeAllSubwindows();
+       mainWindow->clearKeyboard();
+       mainWindow->rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::shutdown()
+{
+       mainWindow.reset();
+       m_updater.close();
+
+       u::log::print("[ui] All windows closed\n");
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::refresh()
+{
+       /* Update dynamic elements inside main window: in and out meters, beat meter
+       and each channel. */
+
+       mainWindow->refresh();
+
+       /* Compute timer for blinker. */
+
+       m_blinker = (m_blinker + 1) % BLINK_RATE;
+
+       /* Refresh Sample Editor and Action Editor for dynamic playhead. */
+
+       refreshSubWindow(WID_SAMPLE_EDITOR);
+       refreshSubWindow(WID_ACTION_EDITOR);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::rebuild()
+{
+       mainWindow->rebuild();
+       rebuildSubWindow(WID_FX_LIST);
+       rebuildSubWindow(WID_SAMPLE_EDITOR);
+       rebuildSubWindow(WID_ACTION_EDITOR);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::rebuildSubWindow(int wid)
+{
+       v::gdWindow* w = getSubwindow(*mainWindow.get(), wid);
+       if (w != nullptr) // If its open
+               w->rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::refreshSubWindow(int wid)
+{
+       v::gdWindow* w = getSubwindow(*mainWindow.get(), wid);
+       if (w != nullptr) // If its open
+               w->refresh();
+}
+
+/* -------------------------------------------------------------------------- */
+
+v::gdWindow* Ui::getSubwindow(v::gdWindow& parent, int wid)
+{
+       return parent.hasWindow(wid) ? parent.getChild(wid) : nullptr;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::openSubWindow(v::gdWindow& parent, v::gdWindow* child, int wid)
+{
+       if (parent.hasWindow(wid))
+       {
+               u::log::print("[GU] parent has subwindow with id=%d, deleting\n", wid);
+               parent.delSubWindow(wid);
+       }
+       child->setId(wid);
+       parent.addSubWindow(child);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::closeSubWindow(int wid)
+{
+       mainWindow->delSubWindow(wid);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::closeAllSubwindows()
+{
+       mainWindow->delSubWindow(WID_ACTION_EDITOR);
+       mainWindow->delSubWindow(WID_SAMPLE_EDITOR);
+       mainWindow->delSubWindow(WID_FX_LIST);
+       mainWindow->delSubWindow(WID_FX);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::setMainWindowTitle(const std::string& s)
+{
+       std::string out = std::string(G_APP_NAME) + " - " + s;
+       mainWindow->copy_label(out.c_str());
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void Ui::startJuceDispatchLoop()
+{
+       Fl::add_timeout(G_GUI_REFRESH_RATE, juceDispatchLoop);
+}
+
+void Ui::stopJuceDispatchLoop()
+{
+       Fl::remove_timeout(juceDispatchLoop);
+}
+
+#endif
+
+/* -------------------------------------------------------------------------- */
+
+void Ui::rebuildStaticWidgets()
+{
+       mainWindow->mainIO->rebuild();
+       mainWindow->mainTimer->rebuild();
+}
+
+/* -------------------------------------------------------------------------- */
+
+#ifdef WITH_VST
+
+void Ui::juceDispatchLoop(void*)
+{
+       juce::MessageManager* mm = juce::MessageManager::getInstanceWithoutCreating();
+       assert(mm != nullptr);
+       mm->runDispatchLoopUntil(1);
+       Fl::add_timeout(G_GUI_REFRESH_RATE, juceDispatchLoop);
+}
+
+#endif
+} // namespace giada::v
diff --git a/src/gui/ui.h b/src/gui/ui.h
new file mode 100644 (file)
index 0000000..116eaf0
--- /dev/null
@@ -0,0 +1,146 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_V_UI_H
+#define G_V_UI_H
+
+#include "core/conf.h"
+#include "core/patch.h"
+#include "gui/dialogs/mainWindow.h"
+#include "gui/dispatcher.h"
+#include "gui/updater.h"
+#include <memory>
+#include <string>
+
+namespace giada::m
+{
+class Recorder;
+class Engine;
+} // namespace giada::m
+
+namespace giada::v
+{
+class Ui final
+{
+public:
+       Ui(m::Recorder&, const m::Conf::Data&);
+
+       /* shouldBlink
+       Return whether is time to blink something or not. This is used to make 
+       widgets blink. */
+
+       bool shouldBlink() const;
+
+       /* load
+       Reads UI information from a Patch when a new project has been loaded. */
+
+       void load(const m::Patch::Data&);
+
+       /* store
+       Writes UI information to a patch when a project needs to be saved. */
+
+       void store(const std::string patchName, m::Patch::Data& patch);
+
+       void init(int argc, char** argv, m::Engine&);
+       void reset();
+       void shutdown();
+
+       /* refresh
+       Repaints dynamic GUI elements. */
+
+       void refresh();
+
+       /* rebuild
+       Rebuilds the UI from scratch. Used when the model has changed. */
+
+       void rebuild();
+
+       /* [rebuild|refresh]SubWindow 
+       Rebuilds or refreshes subwindow with ID 'wid' if it exists, i.e. if it's open. */
+
+       void rebuildSubWindow(int wid);
+       void refreshSubWindow(int wid);
+
+       /* getSubwindow
+       Returns a pointer to an open subwindow, otherwise nullptr. */
+
+       v::gdWindow* getSubwindow(v::gdWindow& parent, int wid);
+
+       /* openSubWindow
+       Opens a new sub-window as a child of parent and assigns 'wid' to child. */
+
+       void openSubWindow(v::gdWindow& parent, v::gdWindow* child, int wid);
+
+       /* closeSubWindow
+       Closes a sun-window currently attached to the main one. */
+
+       void closeSubWindow(int wid);
+
+       /* closeAllSubwindows
+       Closes all subwindows attached to the main one. */
+
+       void closeAllSubwindows();
+
+       /* setMainWindowTitle
+    Updates the title of the main window, usually visible on top of it. */
+
+       void setMainWindowTitle(const std::string&);
+
+#ifdef WITH_VST
+
+       /* [start|stop]JuceDispatchLoop
+       Starts and stops the JUCE dispatch loop from its MessageManager component.
+       This is needed for plugin-ins to wake up their UI editor and let it react
+       to UI events. */
+
+       void startJuceDispatchLoop();
+       void stopJuceDispatchLoop();
+
+#endif
+
+       std::unique_ptr<gdMainWindow> mainWindow;
+       Dispatcher                    dispatcher;
+
+private:
+       static constexpr int BLINK_RATE = G_GUI_FPS / 2;
+
+#ifdef WITH_VST
+       static void juceDispatchLoop(void*);
+#endif
+
+       /* rebuildStaticWidgets
+    Updates attributes of static widgets, i.e. those elements that don't get
+    automatically refreshed during the UI update loop. Useful when loading a new 
+    patch. */
+
+       void rebuildStaticWidgets();
+
+       Updater m_updater;
+       int     m_blinker;
+};
+} // namespace giada::v
+
+#endif
\ No newline at end of file
diff --git a/src/gui/updater.cpp b/src/gui/updater.cpp
new file mode 100644 (file)
index 0000000..cbb2f5a
--- /dev/null
@@ -0,0 +1,76 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "gui/updater.h"
+#include "core/const.h"
+#include "core/model/model.h"
+#include "gui/ui.h"
+#include <FL/Fl.H>
+
+namespace giada::v
+{
+Updater::Updater(Ui& ui)
+: m_ui(ui)
+{
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Updater::init(m::model::Model& model)
+{
+       model.onSwap = [this](m::model::SwapType type) {
+               if (type == m::model::SwapType::NONE)
+                       return;
+
+               /* This callback is fired by the updater thread, so it requires
+               synchronization with the main one. */
+
+               u::gui::ScopedLock lock;
+               type == m::model::SwapType::HARD ? m_ui.rebuild() : m_ui.refresh();
+       };
+
+       Fl::add_timeout(G_GUI_REFRESH_RATE, update, this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Updater::update(void* p) { static_cast<Updater*>(p)->update(); }
+
+/* -------------------------------------------------------------------------- */
+
+void Updater::update()
+{
+       m_ui.refresh();
+       Fl::add_timeout(G_GUI_REFRESH_RATE, update, this);
+}
+
+/* -------------------------------------------------------------------------- */
+
+void Updater::close()
+{
+       Fl::remove_timeout(update);
+}
+} // namespace giada::v
diff --git a/src/gui/updater.h b/src/gui/updater.h
new file mode 100644 (file)
index 0000000..5b592d8
--- /dev/null
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_V_UPDATER_H
+#define G_V_UPDATER_H
+
+namespace giada::m::model
+{
+class Model;
+}
+
+namespace giada::v
+{
+class Ui;
+class Updater final
+{
+public:
+       Updater(Ui& ui);
+
+       void init(m::model::Model&);
+       void close();
+
+private:
+       static void update(void*);
+       void        update();
+
+       Ui& m_ui;
+};
+} // namespace giada::v
+
+#endif
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644 (file)
index 0000000..065002f
--- /dev/null
@@ -0,0 +1,39 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "core/engine.h"
+#include "gui/ui.h"
+
+giada::m::Engine g_engine;
+giada::v::Ui     g_ui(g_engine.recorder, g_engine.conf.data);
+
+int main(int argc, char** argv)
+{
+       if (int ret = giada::m::init::tests(argc, argv); ret != -1)
+               return ret;
+       giada::m::init::startup(argc, argv);
+       return giada::m::init::run();
+}
\ No newline at end of file
diff --git a/src/utils/cocoa.h b/src/utils/cocoa.h
new file mode 100644 (file)
index 0000000..42a5ac7
--- /dev/null
@@ -0,0 +1,43 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * cocoa
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_COCOA_H
+#define G_UTILS_COCOA_H
+
+/* fl_xid() from FLTK returns a pointer to NSWindow, but plugins on OS X want a
+pointer to NSView. The function does the hard conversion. */
+
+void* cocoa_getViewFromWindow(void* p);
+
+/* A bug on on OS X seems to misalign plugins' UI. The function takes care of
+fixing the positioning. 
+TODO temporarily disabled: it does not work. */
+
+//void cocoa_setWindowSize(void *p, int w, int h);
+
+#endif
diff --git a/src/utils/cocoa.mm b/src/utils/cocoa.mm
new file mode 100644 (file)
index 0000000..10792b1
--- /dev/null
@@ -0,0 +1,47 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * cocoa
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2018 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+
+ #import <Foundation/Foundation.h>
+ #import <Cocoa/Cocoa.h>
+ #import "cocoa.h"
+
+
+void* cocoa_getViewFromWindow(void* p)
+{
+  NSWindow* win = (NSWindow* ) p;
+  return (void*) win.contentView;
+}
+
+/*
+void cocoa_setWindowSize(void *p, int w, int h)
+{
+  NSWindow *win = (NSWindow *) p;
+  [win setContentSize:NSMakeSize(w, h)];
+}
+*/
\ No newline at end of file
diff --git a/src/utils/fs.cpp b/src/utils/fs.cpp
new file mode 100644 (file)
index 0000000..6f19795
--- /dev/null
@@ -0,0 +1,195 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * utils
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include <filesystem>
+#if defined(_WIN32) // getcwd (unix) or __getcwd (win)
+#include <direct.h>
+#include <windows.h>
+#else
+#include <unistd.h>
+#endif
+#include <climits>
+#include <cstdarg>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <errno.h>
+#include <string>
+#include <sys/stat.h> // stat (fs::dirExists)
+#ifdef __APPLE__
+#include <libgen.h> // basename unix
+#include <pwd.h>    // getpwuid
+#endif
+#include "core/const.h"
+#include "utils/fs.h"
+#include "utils/log.h"
+#include "utils/string.h"
+
+namespace stdfs = std::filesystem;
+
+namespace giada::u::fs
+{
+bool fileExists(const std::string& s)
+{
+       return stdfs::exists(s);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool isDir(const std::string& s)
+{
+       return stdfs::is_directory(s) && !isProject(s);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool dirExists(const std::string& s)
+{
+       return stdfs::exists(s);
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool mkdir(const std::string& s)
+{
+       return dirExists(s) ? true : stdfs::create_directory(s);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string getRealPath(const std::string& s)
+{
+       return s.empty() || !stdfs::exists(s) ? "" : stdfs::canonical(s).string();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string basename(const std::string& s)
+{
+       return stdfs::path(s).filename().string();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string dirname(const std::string& s)
+{
+       return stdfs::path(s).parent_path().string();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string getCurrentPath()
+{
+       return stdfs::current_path().string();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string getExt(const std::string& s)
+{
+       return stdfs::path(s).extension().string();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string stripExt(const std::string& s)
+{
+       return stdfs::path(s).replace_extension("").string();
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool isProject(const std::string& s)
+{
+       /** TODO - checks too weak. */
+       return getExt(s) == ".gprj";
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string stripFileUrl(const std::string& s)
+{
+       std::string out = s;
+       out             = u::string::replace(out, "file://", "");
+       out             = u::string::replace(out, "%20", " ");
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string getHomePath()
+{
+#if defined(G_OS_LINUX) || defined(G_OS_FREEBSD)
+
+       char buf[PATH_MAX];
+       snprintf(buf, PATH_MAX, "%s/.giada", getenv("HOME"));
+       return stdfs::path(buf).string();
+
+#elif defined(G_OS_WINDOWS)
+
+       return stdfs::current_path().string();
+
+#elif defined(G_OS_MAC)
+
+       char           buf[PATH_MAX];
+       struct passwd* pwd = getpwuid(getuid());
+       if (pwd == nullptr)
+       {
+               log::print("[getHomePath] unable to fetch user infos\n");
+               return "";
+       }
+       const char* home = pwd->pw_dir;
+       snprintf(buf, PATH_MAX, "%s/Library/Application Support/Giada", home);
+
+       return stdfs::path(buf).string();
+
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+bool isRootDir(const std::string& s)
+{
+       return stdfs::current_path().root_directory() == s;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string getUpDir(const std::string& s)
+{
+#ifdef G_OS_WINDOWS
+
+       // If root, let the user browse the drives list by returning "".
+       if (isRootDir(s))
+               return "";
+
+#endif
+
+       return stdfs::path(s).parent_path().string();
+}
+} // namespace giada::u::fs
\ No newline at end of file
diff --git a/src/utils/fs.h b/src/utils/fs.h
new file mode 100644 (file)
index 0000000..b1f608e
--- /dev/null
@@ -0,0 +1,85 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * utils
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_FS_H
+#define G_UTILS_FS_H
+
+#include <string>
+
+namespace giada::u::fs
+{
+bool fileExists(const std::string& s);
+bool dirExists(const std::string& s);
+bool isDir(const std::string& s);
+
+/* isRootDir
+Tells whether 's' is '/' on Unix or '[X]:\' on Windows. */
+
+bool isRootDir(const std::string& s);
+
+bool        isProject(const std::string& s);
+bool        mkdir(const std::string& s);
+std::string getCurrentPath();
+std::string getHomePath();
+
+/* getRealPath
+Expands all symbolic links and resolves references to /./, /../ and extra / 
+characters in the input path and returns the canonicalized absolute pathname. */
+
+std::string getRealPath(const std::string& s);
+
+/* basename
+/path/to/file.txt -> file.txt */
+
+std::string basename(const std::string& s);
+
+/* dirname
+/path/to/file.txt -> /path/to */
+
+std::string dirname(const std::string& s);
+
+/* getExt
+/path/to/file.txt -> txt */
+
+std::string getExt(const std::string& s);
+
+/* stripExt
+/path/to/file.txt -> /path/to/file */
+
+std::string stripExt(const std::string& s);
+
+std::string stripFileUrl(const std::string& s);
+
+/* getUpDir
+Returns the upper directory:
+/path/to/my/directory -> /path/to/my/ */
+
+std::string getUpDir(const std::string& s);
+} // namespace giada::u::fs
+
+#endif
diff --git a/src/utils/gui.cpp b/src/utils/gui.cpp
new file mode 100644 (file)
index 0000000..da5b7a5
--- /dev/null
@@ -0,0 +1,256 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include <FL/Fl.H>
+#include <FL/fl_draw.H>
+#include <cstddef>
+#include <string>
+#if defined(_WIN32)
+#include "../ext/resource.h"
+#elif defined(__linux__) || defined(__FreeBSD__)
+#include <X11/xpm.h>
+#endif
+#include "core/conf.h"
+#include "core/graphics.h"
+#include "core/mixer.h"
+#include "core/mixerHandler.h"
+#include "core/plugins/pluginHost.h"
+#include "core/sequencer.h"
+#include "gui.h"
+#include "gui/dialogs/actionEditor/baseActionEditor.h"
+#include "gui/dialogs/sampleEditor.h"
+#include "gui/dialogs/warnings.h"
+#include "gui/dialogs/window.h"
+#include "gui/elems/mainWindow/keyboard/channel.h"
+#include "gui/elems/mainWindow/keyboard/keyboard.h"
+#include "gui/elems/mainWindow/mainIO.h"
+#include "gui/elems/mainWindow/mainTimer.h"
+#include "gui/elems/mainWindow/mainTransport.h"
+#include "gui/elems/mainWindow/sequencer.h"
+#include "gui/elems/sampleEditor/waveTools.h"
+#include "log.h"
+#include "string.h"
+
+namespace giada::u::gui
+{
+ScopedLock::ScopedLock()
+{
+       Fl::lock();
+}
+
+/* -------------------------------------------------------------------------- */
+
+ScopedLock::~ScopedLock()
+{
+       Fl::unlock();
+}
+
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+/* -------------------------------------------------------------------------- */
+
+void setFavicon(v::gdWindow* w)
+{
+#if defined(__linux__) || defined(__FreeBSD__)
+
+       fl_open_display();
+       Pixmap p, mask;
+       XpmCreatePixmapFromData(fl_display, DefaultRootWindow(fl_display),
+           (char**)giada_icon, &p, &mask, nullptr);
+       w->icon((char*)p);
+
+#elif defined(_WIN32)
+
+       w->icon((char*)LoadIcon(fl_display, MAKEINTRESOURCE(IDI_ICON1)));
+
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+geompp::Rect<int> getStringRect(const std::string& s)
+{
+       int w = 0;
+       int h = 0;
+       fl_measure(s.c_str(), w, h);
+       return {0, 0, w, h};
+}
+
+/* -------------------------------------------------------------------------- */
+
+geompp::Rect<int> getCenterWinBounds(int w, int h)
+{
+       return {centerWindowX(w), centerWindowY(h), w, h};
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string removeFltkChars(const std::string& s)
+{
+       std::string out = u::string::replace(s, "/", "-");
+       out             = u::string::replace(out, "|", "-");
+       out             = u::string::replace(out, "&", "-");
+       out             = u::string::replace(out, "_", "-");
+       return out;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string truncate(const std::string& s, Pixel width)
+{
+       if (s.empty() || getStringRect(s).w <= width)
+               return s;
+
+       std::string tmp  = s;
+       std::size_t size = tmp.size();
+
+       while (getStringRect(tmp + "...").w > width)
+       {
+               if (size == 0)
+                       return "";
+               tmp.resize(--size);
+       }
+
+       return tmp + "...";
+}
+
+/* -------------------------------------------------------------------------- */
+
+int centerWindowX(int w)
+{
+       return (Fl::w() / 2) - (w / 2);
+}
+
+int centerWindowY(int h)
+{
+       return (Fl::h() / 2) - (h / 2);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string keyToString(int key)
+{
+       // https://github.com/fltk/fltk/blob/570a05a33c9dc42a16caa5a1a11cf34d4df1c1f9/FL/Enumerations.H
+       // https://www.fltk.org/doc-1.3/group__fl__events.html#gafa17a5b4d8d9163631c88142e60447ed
+
+       if (key == 0)
+               return "[None]";
+
+       switch (key)
+       {
+       case ' ':
+               return "Space";
+       case FL_BackSpace:
+               return "Backspace";
+       case FL_Tab:
+               return "Tab";
+       case FL_Enter:
+               return "Enter";
+       case FL_Pause:
+               return "Pause";
+       case FL_Scroll_Lock:
+               return "Scroll lock";
+       case FL_Escape:
+               return "Escape";
+       case FL_Home:
+               return "Home";
+       case FL_Left:
+               return "Left";
+       case FL_Up:
+               return "Up";
+       case FL_Right:
+               return "Right";
+       case FL_Down:
+               return "Down";
+       case FL_Page_Up:
+               return "Page up";
+       case FL_Page_Down:
+               return "Page down";
+       case FL_End:
+               return "End";
+       case FL_Print:
+               return "Print";
+       case FL_Insert:
+               return "Insert";
+       case FL_Menu:
+               return "Menu";
+       case FL_Help:
+               return "Help";
+       case FL_Num_Lock:
+               return "Num lock";
+       case FL_KP: // TODO ?
+               return "";
+       case FL_KP_Enter:
+               return "KP Enter";
+       case FL_F + 1:
+               return "F1";
+       case FL_F + 2:
+               return "F2";
+       case FL_F + 3:
+               return "F3";
+       case FL_F + 4:
+               return "F4";
+       case FL_F + 5:
+               return "F5";
+       case FL_F + 6:
+               return "F6";
+       case FL_F + 7:
+               return "F7";
+       case FL_F + 8:
+               return "F8";
+       case FL_F + 9:
+               return "F9";
+       case FL_F + 10:
+               return "F10";
+       case FL_F + 11:
+               return "F11";
+       case FL_F + 12:
+               return "F12";
+       case FL_Shift_L:
+               return "Shift L";
+       case FL_Shift_R:
+               return "Shift R";
+       case FL_Control_L:
+               return "Control L";
+       case FL_Control_R:
+               return "Control R";
+       case FL_Caps_Lock:
+               return "Caps lock";
+       case FL_Meta_L:
+               return "Meta L";
+       case FL_Meta_R:
+               return "Meta R";
+       case FL_Alt_L:
+               return "Alt L";
+       case FL_Alt_R:
+               return "Alt R";
+       case FL_Delete:
+               return "Delete";
+       default:
+               return Fl::event_text();
+       }
+}
+} // namespace giada::u::gui
diff --git a/src/utils/gui.h b/src/utils/gui.h
new file mode 100644 (file)
index 0000000..dec6721
--- /dev/null
@@ -0,0 +1,93 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_GUI_H
+#define G_UTILS_GUI_H
+
+#include "core/types.h"
+#include "deps/geompp/src/rect.hpp"
+#include <FL/Fl_Menu_Item.H>
+#include <string>
+
+namespace giada::v
+{
+class gdWindow;
+}
+
+namespace giada::u::gui
+{
+/* ScopedLock
+ScopedLock for locking the main FLTK thread when the UI must be updated from a
+secondary thread. */
+
+class ScopedLock
+{
+public:
+       ScopedLock();
+       ~ScopedLock();
+};
+
+void setFavicon(v::gdWindow* w);
+
+/* removeFltkChars
+Strips special chars used by FLTK to split menus into sub-menus. */
+
+std::string removeFltkChars(const std::string& s);
+
+/* getStringRect
+Returns the bounding box in pixels of a string 's'. */
+
+geompp::Rect<int> getStringRect(const std::string& s);
+
+/* getCenterWinBounds
+Returns the bounding box to be used for a centered window. */
+
+geompp::Rect<int> getCenterWinBounds(int w, int h);
+
+/* truncate
+Adds ellipsis to a string 's' if it longer than 'width' pixels. */
+
+std::string truncate(const std::string& s, Pixel width);
+
+int centerWindowX(int w);
+int centerWindowY(int h);
+
+/* keyToString
+Translates an FLTK key event into a human-readable string. */
+
+std::string keyToString(int key);
+
+/* makeMenuItem
+Makes a new Fl_Menu_Item at compile time. Used to initialize pop-up menus. */
+
+constexpr Fl_Menu_Item makeMenuItem(const char* text, Fl_Callback* callback = nullptr,
+    void* data = nullptr, int flags = 0)
+{
+       return Fl_Menu_Item{text, 0, callback, data, flags, 0, 0, 0, 0};
+}
+} // namespace giada::u::gui
+
+#endif
diff --git a/src/utils/log.cpp b/src/utils/log.cpp
new file mode 100644 (file)
index 0000000..42a1d0f
--- /dev/null
@@ -0,0 +1,59 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * log
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "log.h"
+#include <cstdio>
+#include <string>
+
+namespace giada::u::log
+{
+int init(int m)
+{
+       mode = m;
+       stat = true;
+       if (mode == LOG_MODE_FILE)
+       {
+               std::string fpath = fs::getHomePath() + G_SLASH + "giada.log";
+               f                 = std::fopen(fpath.c_str(), "a");
+               if (!f)
+               {
+                       stat = false;
+                       return 0;
+               }
+       }
+       return 1;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void close()
+{
+       if (mode == LOG_MODE_FILE)
+               std::fclose(f);
+}
+} // namespace giada::u::log
diff --git a/src/utils/log.h b/src/utils/log.h
new file mode 100644 (file)
index 0000000..2413724
--- /dev/null
@@ -0,0 +1,94 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * log
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_LOG_H
+#define G_UTILS_LOG_H
+
+#include "core/const.h"
+#include "utils/fs.h"
+#include <cstdio>
+#include <string>
+#include <type_traits>
+#include <utility>
+
+namespace giada::u::log
+{
+inline FILE* f;
+inline int   mode;
+inline bool  stat;
+
+/* init
+Initializes logger. Mode defines where to write the output: LOG_MODE_STDOUT,
+LOG_MODE_FILE and LOG_MODE_MUTE. */
+
+int init(int mode);
+
+void close();
+
+/* string_to_c_str
+Internal utility function for string transformation. Uses forwarding references
+(&&) to avoid useless string copy. */
+
+static constexpr auto string_to_c_str = [](auto&& s) {
+       /* Remove any reference and const-ness, since the function can handle 
+       l-value and r-value, const or not. TODO - Use std::remove_cvref instead, 
+       when switching to C++20. */
+       if constexpr (std::is_same_v<std::remove_const_t<std::remove_reference_t<
+                                        decltype(s)>>,
+                         std::string>)
+               // If the argument is a std::string return an old-style C-string
+               return s.c_str();
+       else
+               // Return the argument unchanged otherwise
+               return s;
+};
+
+/* print
+A variadic printf-like logging function. Any `std::string` argument will be 
+automatically transformed into a C-string. */
+
+template <typename... Args>
+static void print(const char* format, Args&&... args)
+{
+       if (mode == LOG_MODE_MUTE)
+               return;
+
+       if (mode == LOG_MODE_FILE && stat == true)
+       {
+               // Replace any std::string in the arguments by its C-string
+               std::fprintf(f, format, string_to_c_str(std::forward<Args>(args))...);
+#ifdef _WIN32
+               fflush(f);
+#endif
+       }
+       else
+               std::printf(format, string_to_c_str(std::forward<Args>(args))...);
+}
+} // namespace giada::u::log
+
+#endif
diff --git a/src/utils/math.cpp b/src/utils/math.cpp
new file mode 100644 (file)
index 0000000..9f38a2f
--- /dev/null
@@ -0,0 +1,59 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "math.h"
+#include <cmath>
+
+namespace giada
+{
+namespace u
+{
+namespace math
+{
+float linearToDB(float f)
+{
+       return 20 * std::log10(f);
+}
+
+/* -------------------------------------------------------------------------- */
+
+int quantize(int x, int step)
+{
+       /* Source:
+       https://en.wikipedia.org/wiki/Quantization_(signal_processing)#Rounding_example */
+       return step * std::floor((x / (float)step) + 0.5f);
+}
+
+/* -------------------------------------------------------------------------- */
+
+float dBtoLinear(float f)
+{
+       return std::pow(10, f / 20.0f);
+}
+
+} // namespace math
+} // namespace u
+} // namespace giada
\ No newline at end of file
diff --git a/src/utils/math.h b/src/utils/math.h
new file mode 100644 (file)
index 0000000..7c31338
--- /dev/null
@@ -0,0 +1,66 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_MATH_H
+#define G_UTILS_MATH_H
+
+#include <cassert>
+#include <type_traits>
+
+namespace giada::u::math
+{
+float linearToDB(float f);
+float dBtoLinear(float f);
+int   quantize(int x, int step);
+
+/* -------------------------------------------------------------------------- */
+
+/* map (1)
+Maps 'x' in range [a, b] to a new range [w, z]. Source:
+       https://en.wikipedia.org/wiki/Linear_equation#Two-point_form*/
+
+template <typename TI, typename TO>
+TO map(TI x, TI a, TI b, TO w, TO z)
+{
+       static_assert(std::is_arithmetic_v<TI>);
+       static_assert(std::is_arithmetic_v<TO>);
+
+       if (a == b) // Prevents division by zero (undefined behavior)
+               return x;
+       return (((x - a) / (double)(b - a)) * (z - w)) + w;
+}
+
+/* map (2)
+Maps 'x' in range [0, b) to a new range [0, z]. */
+
+template <typename TI, typename TO>
+TO map(TI x, TI b, TO z)
+{
+       return map(x, static_cast<TI>(0), b, static_cast<TO>(0), z);
+}
+} // namespace giada::u::math
+
+#endif
diff --git a/src/utils/string.cpp b/src/utils/string.cpp
new file mode 100644 (file)
index 0000000..f50c858
--- /dev/null
@@ -0,0 +1,122 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * utils
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "string.h"
+#include "core/const.h"
+#include <climits>
+#include <cstdarg>
+#include <cstddef>
+#include <iomanip>
+#include <memory>
+
+namespace giada
+{
+namespace u
+{
+namespace string
+{
+/* TODO - use std::to_string() */
+
+std::string fToString(float f, int precision)
+{
+       std::stringstream out;
+       out << std::fixed << std::setprecision(precision) << f;
+       return out.str();
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string trim(const std::string& s)
+{
+       std::size_t first = s.find_first_not_of(" \n\t");
+       std::size_t last  = s.find_last_not_of(" \n\t");
+       return s.substr(first, last - first + 1);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string replace(std::string in, const std::string& search, const std::string& replace)
+{
+       std::size_t pos = 0;
+       while ((pos = in.find(search, pos)) != std::string::npos)
+       {
+               in.replace(pos, search.length(), replace);
+               pos += replace.length();
+       }
+       return in;
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string format(const char* format, ...)
+{
+       va_list args;
+
+       /* Compute the size of the new expanded std::string (i.e. with replacement taken
+       into account). */
+
+       va_start(args, format);
+       std::size_t size = vsnprintf(nullptr, 0, format, args) + 1;
+       va_end(args);
+
+       /* Create a new temporary char array to hold the new expanded std::string. */
+
+       std::unique_ptr<char[]> tmp(new char[size]);
+
+       /* Fill the temporary std::string with the formatted data. */
+
+       va_start(args, format);
+       vsprintf(tmp.get(), format, args);
+       va_end(args);
+
+       return std::string(tmp.get(), tmp.get() + size - 1);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::vector<std::string> split(std::string in, std::string sep)
+{
+       std::vector<std::string> out;
+       std::string              full  = in;
+       std::string              token = "";
+       std::size_t              curr  = 0;
+       std::size_t              next  = -1;
+       do
+       {
+               curr  = next + 1;
+               next  = full.find_first_of(sep, curr);
+               token = full.substr(curr, next - curr);
+               if (token != "")
+                       out.push_back(token);
+       } while (next != std::string::npos);
+       return out;
+}
+
+} // namespace string
+} // namespace u
+} // namespace giada
diff --git a/src/utils/string.h b/src/utils/string.h
new file mode 100644 (file)
index 0000000..a4d5a8d
--- /dev/null
@@ -0,0 +1,68 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * utils
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_STRING_H
+#define G_UTILS_STRING_H
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace giada
+{
+namespace u
+{
+namespace string
+{
+template <typename T>
+std::string iToString(T t, bool hex = false)
+{
+       std::stringstream out;
+       if (hex)
+               out << std::hex << std::uppercase << t;
+       else
+               out << t;
+       return out.str();
+}
+
+std::string replace(std::string in, const std::string& search,
+    const std::string& replace);
+
+std::string trim(const std::string& s);
+
+std::vector<std::string> split(std::string in, std::string sep);
+
+std::string fToString(float f, int precision);
+
+std::string format(const char* format, ...);
+
+} // namespace string
+} // namespace u
+} // namespace giada
+
+#endif
diff --git a/src/utils/time.cpp b/src/utils/time.cpp
new file mode 100644 (file)
index 0000000..b95c467
--- /dev/null
@@ -0,0 +1,45 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * utils
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "time.h"
+#include <chrono>
+#include <thread>
+
+namespace giada
+{
+namespace u
+{
+namespace time
+{
+void sleep(int millisecs)
+{
+       std::this_thread::sleep_for(std::chrono::milliseconds(millisecs));
+}
+} // namespace time
+} // namespace u
+} // namespace giada
diff --git a/src/utils/time.h b/src/utils/time.h
new file mode 100644 (file)
index 0000000..f79c345
--- /dev/null
@@ -0,0 +1,43 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * utils
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_TIME_H
+#define G_UTILS_TIME_H
+
+namespace giada
+{
+namespace u
+{
+namespace time
+{
+void sleep(int millisecs);
+}
+} // namespace u
+} // namespace giada
+
+#endif
\ No newline at end of file
diff --git a/src/utils/vector.h b/src/utils/vector.h
new file mode 100644 (file)
index 0000000..dfbcace
--- /dev/null
@@ -0,0 +1,90 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_VECTOR_H
+#define G_UTILS_VECTOR_H
+
+#include <cstddef>
+#include <algorithm>
+#include <functional>
+#include <vector>
+
+namespace giada::u::vector
+{
+template <typename T, typename P>
+std::size_t indexOf(const T& v, const P& p)
+{
+       return std::distance(std::cbegin(v), std::find(std::cbegin(v), std::cend(v), p));
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T, typename F>
+auto findIf(const T& v, F&& func)
+{
+       return std::find_if(std::cbegin(v), std::cend(v), func);
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T, typename F>
+bool has(const T& v, F&& func)
+{
+       return findIf(v, func) != std::cend(v);
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T, typename F>
+void removeIf(T& v, F&& func)
+{
+       v.erase(std::remove_if(v.begin(), v.end(), func), v.end());
+}
+
+template <typename T, typename V>
+void remove(T& v, const V& o)
+{
+       v.erase(std::remove(v.begin(), v.end(), o), v.end());
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename T, typename I>
+std::vector<T> cast(const I& i)
+{
+       return {i.begin(), i.end()};
+}
+
+/* -------------------------------------------------------------------------- */
+
+template <typename Vector, typename Default>
+auto atOr(const Vector& v, int index, Default d)
+{
+       return index >= 0 && static_cast<size_t>(index) < v.size() ? v[index] : d;
+}
+} // namespace giada::u::vector
+
+#endif
diff --git a/src/utils/ver.cpp b/src/utils/ver.cpp
new file mode 100644 (file)
index 0000000..a4c0470
--- /dev/null
@@ -0,0 +1,69 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#include "ver.h"
+#include "core/const.h"
+#include "deps/rtaudio/RtAudio.h"
+#include <RtMidi.h>
+#include <sndfile.h>
+
+namespace giada
+{
+namespace u
+{
+namespace ver
+{
+std::string getLibsndfileVersion()
+{
+       char buffer[128];
+       sf_command(nullptr, SFC_GET_LIB_VERSION, buffer, sizeof(buffer));
+       return std::string(buffer);
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string getRtAudioVersion()
+{
+#ifdef TESTS
+       return "";
+#else
+       return RtAudio::getVersion();
+#endif
+}
+
+/* -------------------------------------------------------------------------- */
+
+std::string getRtMidiVersion()
+{
+#ifdef TESTS
+       return "";
+#else
+       return RtMidi::getVersion();
+#endif
+}
+} // namespace ver
+} // namespace u
+} // namespace giada
diff --git a/src/utils/ver.h b/src/utils/ver.h
new file mode 100644 (file)
index 0000000..7f96f85
--- /dev/null
@@ -0,0 +1,45 @@
+/* -----------------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2022 Giovanni A. Zuliani | Monocasual Laboratories
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine 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.
+ *
+ * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- */
+
+#ifndef G_UTILS_VER_H
+#define G_UTILS_VER_H
+
+#include <string>
+
+namespace giada
+{
+namespace u
+{
+namespace ver
+{
+std::string getLibsndfileVersion();
+std::string getRtAudioVersion();
+std::string getRtMidiVersion();
+} // namespace ver
+} // namespace u
+} // namespace giada
+
+#endif
diff --git a/tests/actionRecorder.cpp b/tests/actionRecorder.cpp
new file mode 100644 (file)
index 0000000..e0f884c
--- /dev/null
@@ -0,0 +1,69 @@
+#include "src/core/actions/actionRecorder.h"
+#include "src/core/actions/action.h"
+#include "src/core/actions/actions.h"
+#include "src/core/const.h"
+#include "src/core/model/model.h"
+#include "src/core/types.h"
+#include <catch2/catch.hpp>
+
+TEST_CASE("ActionRecorder")
+{
+       using namespace giada;
+       using namespace giada::m;
+
+       model::Model   model;
+       ActionRecorder ar(model);
+
+       REQUIRE(ar.hasActions(/*ch=*/0) == false);
+
+       SECTION("Test record")
+       {
+               const int       ch = 0;
+               const Frame     f1 = 10;
+               const Frame     f2 = 70;
+               const MidiEvent e1 = MidiEvent(MidiEvent::NOTE_ON, 0x00, 0x00);
+               const MidiEvent e2 = MidiEvent(MidiEvent::NOTE_OFF, 0x00, 0x00);
+
+               const Action a1 = ar.rec(ch, f1, e1);
+               const Action a2 = ar.rec(ch, f2, e2);
+
+               REQUIRE(ar.hasActions(ch) == true);
+               REQUIRE(a1.frame == f1);
+               REQUIRE(a2.frame == f2);
+               REQUIRE(a1.prevId == 0);
+               REQUIRE(a1.nextId == 0);
+               REQUIRE(a2.prevId == 0);
+               REQUIRE(a2.nextId == 0);
+
+               SECTION("Test clear actions by channel")
+               {
+                       const int       ch = 1;
+                       const Frame     f1 = 100;
+                       const Frame     f2 = 200;
+                       const MidiEvent e1 = MidiEvent(MidiEvent::NOTE_ON, 0x00, 0x00);
+                       const MidiEvent e2 = MidiEvent(MidiEvent::NOTE_OFF, 0x00, 0x00);
+
+                       ar.rec(ch, f1, e1);
+                       ar.rec(ch, f2, e2);
+
+                       ar.clearChannel(/*channel=*/0);
+
+                       REQUIRE(ar.hasActions(/*channel=*/0) == false);
+                       REQUIRE(ar.hasActions(/*channel=*/1) == true);
+               }
+
+               SECTION("Test clear actions by type")
+               {
+                       ar.clearActions(/*channel=*/0, MidiEvent::NOTE_ON);
+                       ar.clearActions(/*channel=*/0, MidiEvent::NOTE_OFF);
+
+                       REQUIRE(ar.hasActions(/*channel=*/0) == false);
+               }
+
+               SECTION("Test clear all")
+               {
+                       ar.clearAllActions();
+                       REQUIRE(ar.hasActions(/*channel=*/0) == false);
+               }
+       }
+}
diff --git a/tests/main.cpp b/tests/main.cpp
new file mode 100644 (file)
index 0000000..b76f5ca
--- /dev/null
@@ -0,0 +1,3 @@
+#define CATCH_CONFIG_MAIN
+#define CATCH_CONFIG_FAST_COMPILE
+#include <catch2/catch.hpp>
\ No newline at end of file
diff --git a/tests/midiLighter.cpp b/tests/midiLighter.cpp
new file mode 100644 (file)
index 0000000..fc729be
--- /dev/null
@@ -0,0 +1,84 @@
+#include "../src/core/channels/midiLighter.h"
+#include "mocks/kernelMidiMock.h"
+#include <catch2/catch.hpp>
+#include <memory>
+
+TEST_CASE("MidiMapper")
+{
+       using namespace giada;
+
+       m::KernelMidiMock                kernelMidi;
+       m::MidiMapper<m::KernelMidiMock> midiMapper(kernelMidi);
+       m::MidiLighter                   midiLighter(midiMapper);
+
+       midiMapper.currentMap = {
+           "test-brand",
+           "test-device",
+           {{0, "0x000000", 0, 0x000000}}, // init commands
+           {0, "0x000001", 0, 0x000001},   // mute on
+           {0, "0x000002", 0, 0x000002},   // mute off
+           {0, "0x000003", 0, 0x000003},   // solo on
+           {0, "0x000004", 0, 0x000004},   // solo off
+           {0, "0x000005", 0, 0x000005},   // waiting
+           {0, "0x000006", 0, 0x000006},   // playing
+           {0, "0x000007", 0, 0x000007},   // stopping
+           {0, "0x000008", 0, 0x000008},   // stopped
+           {0, "0x000009", 0, 0x000009},   // playingInaudible
+       };
+
+       midiLighter.onSend = []() {};
+
+       midiLighter.playing = {0x000010, 0};
+       midiLighter.mute    = {0x000011, 0};
+       midiLighter.solo    = {0x000012, 0};
+
+       SECTION("Test initialization")
+       {
+               REQUIRE(midiLighter.enabled == false);
+       }
+
+       SECTION("Test send OFF status")
+       {
+               midiLighter.sendStatus(ChannelStatus::OFF, /*audible=*/true);
+               REQUIRE(kernelMidi.sent.back() == 0x000008); // Stopped
+       }
+
+       SECTION("Test send WAIT status")
+       {
+               midiLighter.sendStatus(ChannelStatus::WAIT, /*audible=*/true);
+               REQUIRE(kernelMidi.sent.back() == 0x000005); // Waiting
+       }
+
+       SECTION("Test send ENDING status")
+       {
+               midiLighter.sendStatus(ChannelStatus::ENDING, /*audible=*/true);
+               REQUIRE(kernelMidi.sent.back() == 0x000007); // Stopping
+       }
+
+       SECTION("Test send PLAY status")
+       {
+               midiLighter.sendStatus(ChannelStatus::PLAY, /*audible=*/true);
+               REQUIRE(kernelMidi.sent.back() == 0x000006); // Playing
+
+               midiLighter.sendStatus(ChannelStatus::PLAY, /*audible=*/false);
+               REQUIRE(kernelMidi.sent.back() == 0x000009); // Playing inaudible
+       }
+
+       SECTION("Test send mute")
+       {
+               midiLighter.sendMute(/*isMuted=*/true);
+               REQUIRE(kernelMidi.sent.back() == 0x000001); // Mute on
+
+               midiLighter.sendMute(/*isMuted=*/false);
+               REQUIRE(kernelMidi.sent.back() == 0x000002); // Mute off
+       }
+
+       SECTION("Test send solo")
+       {
+               midiLighter.sendSolo(/*isSoloed=*/true);
+               REQUIRE(kernelMidi.sent.back() == 0x000003); // Solo on
+
+               midiLighter.sendSolo(/*isSoloed=*/false);
+               REQUIRE(kernelMidi.sent.back() == 0x000004); // Solo off
+       }
+}
diff --git a/tests/mocks/kernelMidiMock.h b/tests/mocks/kernelMidiMock.h
new file mode 100644 (file)
index 0000000..8929b1a
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef G_TESTS_KERNELMIDI_MOCK_H
+#define G_TESTS_KERNELMIDI_MOCK_H
+
+namespace giada::m
+{
+class KernelMidiMock
+{
+public:
+       void send(uint32_t s)
+       {
+               sent.push_back(s);
+       }
+
+       std::vector<uint32_t> sent;
+};
+} // namespace giada::m
+
+#endif
\ No newline at end of file
diff --git a/tests/resources/test.wav b/tests/resources/test.wav
new file mode 100644 (file)
index 0000000..1e52d36
Binary files /dev/null and b/tests/resources/test.wav differ
diff --git a/tests/samplePlayer.cpp b/tests/samplePlayer.cpp
new file mode 100644 (file)
index 0000000..1c6a267
--- /dev/null
@@ -0,0 +1,91 @@
+#include "../src/core/channels/samplePlayer.h"
+#include <catch2/catch.hpp>
+
+TEST_CASE("SamplePlayer")
+{
+       using namespace giada;
+
+       constexpr int BUFFER_SIZE  = 1024;
+       constexpr int NUM_CHANNELS = 2;
+
+       // Wave values: [1..BUFFERSIZE*4]
+       m::Wave wave(0);
+       wave.getBuffer().alloc(BUFFER_SIZE * 4, NUM_CHANNELS);
+       wave.getBuffer().forEachFrame([](float* f, int i) {
+               f[0] = static_cast<float>(i + 1);
+               f[1] = static_cast<float>(i + 1);
+       });
+
+       m::ChannelShared channelShared(BUFFER_SIZE);
+       m::Resampler     resampler(m::Resampler::Quality::LINEAR, NUM_CHANNELS);
+
+       m::SamplePlayer samplePlayer(&resampler);
+       samplePlayer.onLastFrame = [](bool) {};
+
+       SECTION("Test initialization")
+       {
+               REQUIRE(samplePlayer.hasWave() == false);
+       }
+
+       SECTION("Test rendering")
+       {
+               samplePlayer.loadWave(channelShared, &wave);
+
+               REQUIRE(samplePlayer.hasWave() == true);
+               REQUIRE(samplePlayer.begin == 0);
+               REQUIRE(samplePlayer.end == wave.getBuffer().countFrames() - 1);
+
+               REQUIRE(channelShared.tracker.load() == 0);
+               REQUIRE(channelShared.playStatus.load() == ChannelStatus::OFF);
+
+               for (const float pitch : {1.0f, 0.5f})
+               {
+                       samplePlayer.pitch = pitch;
+
+                       SECTION("Sub-range [M, N), pitch == " + std::to_string(pitch))
+                       {
+                               constexpr int RANGE_BEGIN = 16;
+                               constexpr int RANGE_END   = 48;
+
+                               samplePlayer.begin = RANGE_BEGIN;
+                               samplePlayer.end   = RANGE_END;
+                               samplePlayer.render(channelShared, {});
+
+                               int numFramesWritten = 0;
+                               channelShared.audioBuffer.forEachFrame([&numFramesWritten](float* f, int) {
+                                       if (f[0] != 0.0)
+                                               numFramesWritten++;
+                               });
+
+                               REQUIRE(numFramesWritten == (RANGE_END - RANGE_BEGIN) / pitch);
+                       }
+
+                       SECTION("Rewind, pitch == " + std::to_string(pitch))
+                       {
+                               // Point in audio buffer where the rewind takes place
+                               const int OFFSET = 256;
+
+                               samplePlayer.render(channelShared, {m::SamplePlayer::Render::Mode::REWIND, OFFSET});
+
+                               // Rendering should start over again at buffer[OFFSET]
+                               REQUIRE(channelShared.audioBuffer[OFFSET][0] == 1.0f);
+                       }
+
+                       SECTION("Stop, pitch == " + std::to_string(pitch))
+                       {
+                               // Point in audio buffer where the stop takes place
+                               const int OFFSET = 256;
+
+                               samplePlayer.render(channelShared, {m::SamplePlayer::Render::Mode::STOP, OFFSET});
+
+                               int numFramesWritten = 0;
+                               channelShared.audioBuffer.forEachFrame([&numFramesWritten](float* f, int) {
+                                       if (f[0] != 0.0)
+                                               numFramesWritten++;
+                               });
+
+                               REQUIRE(numFramesWritten == OFFSET);
+                       }
+               }
+       }
+}
diff --git a/tests/utils.cpp b/tests/utils.cpp
new file mode 100644 (file)
index 0000000..4ec2a26
--- /dev/null
@@ -0,0 +1,64 @@
+#include "../src/utils/fs.h"
+#include "../src/utils/math.h"
+#include "../src/utils/string.h"
+#include <catch2/catch.hpp>
+
+TEST_CASE("u::fs")
+{
+       using namespace giada::u;
+
+       REQUIRE(fs::fileExists(TEST_RESOURCES_DIR "test.wav") == true);
+       REQUIRE(fs::fileExists("nonexistent_file") == false);
+       REQUIRE(fs::dirExists(TEST_RESOURCES_DIR) == true);
+       REQUIRE(fs::dirExists("ghost_dir/") == false);
+       REQUIRE(fs::isDir(TEST_RESOURCES_DIR) == true);
+       REQUIRE(fs::isDir("nonexistent_dir") == false);
+       REQUIRE(fs::basename("tests/utils.cpp") == "utils.cpp");
+       REQUIRE(fs::dirname("tests/utils.cpp") == "tests");
+       REQUIRE(fs::getExt("tests/utils.cpp") == ".cpp");
+       REQUIRE(fs::stripExt("tests/utils.cpp") == "tests/utils");
+#if defined(_WIN32)
+       REQUIRE(fs::isRootDir("\\") == true);
+       REQUIRE(fs::isRootDir("C:\\path\\to\\something") == false);
+       REQUIRE(fs::getUpDir("C:\\path\\to\\something") == "C:\\path\\to");
+       REQUIRE(fs::getUpDir("C:\\path") == "C:\\");
+       REQUIRE(fs::getUpDir("C:\\") == "C:\\");
+#else
+       REQUIRE(fs::isRootDir("/") == true);
+       REQUIRE(fs::isRootDir("/path/to/something") == false);
+       REQUIRE(fs::getUpDir("/path/to/something") == "/path/to");
+       REQUIRE(fs::getUpDir("/path") == "/");
+       REQUIRE(fs::getUpDir("/") == "/");
+#endif
+}
+
+TEST_CASE("u::string")
+{
+       using namespace giada::u;
+
+       REQUIRE(string::replace("Giada is cool", "cool", "hot") == "Giada is hot");
+       REQUIRE(string::trim("   Giada is cool       ") == "Giada is cool");
+       REQUIRE(string::iToString(666) == "666");
+       REQUIRE(string::iToString(0x99AABB, true) == "99AABB");
+       REQUIRE(string::fToString(3.14159, 2) == "3.14");
+       REQUIRE(string::format("I see %d men with %s hats", 5, "strange") == "I see 5 men with strange hats");
+
+       std::vector<std::string> v = string::split("Giada is cool", " ");
+       REQUIRE(v.size() == 3);
+       REQUIRE(v.at(0) == "Giada");
+       REQUIRE(v.at(1) == "is");
+       REQUIRE(v.at(2) == "cool");
+}
+
+TEST_CASE("::math")
+{
+       using namespace giada::u;
+
+       REQUIRE(math::map(0.0f, 0.0f, 30.0f, 0.0f, 1.0f) == 0.0f);
+       REQUIRE(math::map(30.0f, 0.0f, 30.0f, 0.0f, 1.0f) == 1.0f);
+       REQUIRE(math::map(15.0f, 0.0f, 30.0f, 0.0f, 1.0f) == Approx(0.5f));
+
+       REQUIRE(math::map(0.0f, 30.0f, 1.0f) == 0.0f);
+       REQUIRE(math::map(30.0f, 30.0f, 1.0f) == 1.0f);
+       REQUIRE(math::map(15.0f, 30.0f, 1.0f) == Approx(0.5f));
+}
diff --git a/tests/wave.cpp b/tests/wave.cpp
new file mode 100644 (file)
index 0000000..0a622f8
--- /dev/null
@@ -0,0 +1,47 @@
+#include "../src/core/wave.h"
+#include <catch2/catch.hpp>
+#include <memory>
+
+TEST_CASE("Wave")
+{
+       using namespace giada;
+
+       static const int SAMPLE_RATE = 44100;
+       static const int BUFFER_SIZE = 4096;
+       static const int CHANNELS    = 2;
+       static const int BIT_DEPTH   = 32;
+
+       /* Each SECTION the TEST_CASE is executed from the start. Any code between 
+       this comment and the first SECTION macro is executed before each SECTION. */
+
+       SECTION("test allocation")
+       {
+               m::Wave wave(1);
+               wave.alloc(BUFFER_SIZE, CHANNELS, SAMPLE_RATE, BIT_DEPTH, "path/to/sample.wav");
+
+               SECTION("test basename")
+               {
+                       REQUIRE(wave.getPath() == "path/to/sample.wav");
+                       REQUIRE(wave.getBasename() == "sample");
+                       REQUIRE(wave.getBasename(true) == "sample.wav");
+               }
+
+               SECTION("test path")
+               {
+                       wave.setPath("path/is/now/different.mp3");
+
+                       REQUIRE(wave.getPath() == "path/is/now/different.mp3");
+
+                       wave.setPath("path/is/now/different.mp3", 5);
+
+                       REQUIRE(wave.getPath() == "path/is/now/different-5.mp3");
+               }
+
+               SECTION("test change name")
+               {
+                       REQUIRE(wave.getPath() == "path/to/sample.wav");
+                       REQUIRE(wave.getBasename() == "sample");
+                       REQUIRE(wave.getBasename(true) == "sample.wav");
+               }
+       }
+}
diff --git a/tests/waveFx.cpp b/tests/waveFx.cpp
new file mode 100644 (file)
index 0000000..f045846
--- /dev/null
@@ -0,0 +1,102 @@
+#include "../src/core/waveFx.h"
+#include "../src/core/const.h"
+#include "../src/core/types.h"
+#include "../src/core/wave.h"
+#include <catch2/catch.hpp>
+#include <memory>
+
+using namespace giada;
+using namespace giada::m;
+
+TEST_CASE("waveFx")
+{
+       static const int SAMPLE_RATE = 44100;
+       static const int BUFFER_SIZE = 4000;
+       static const int BIT_DEPTH   = 32;
+
+       Wave waveMono(0);
+       Wave waveStereo(0);
+
+       waveMono.alloc(BUFFER_SIZE, 1, SAMPLE_RATE, BIT_DEPTH, "path/to/sample-mono.wav");
+       waveStereo.alloc(BUFFER_SIZE, 2, SAMPLE_RATE, BIT_DEPTH, "path/to/sample-stereo.wav");
+
+       SECTION("test mono->stereo conversion")
+       {
+               int prevSize = waveMono.getBuffer().countFrames();
+
+               REQUIRE(wfx::monoToStereo(waveMono) == G_RES_OK);
+               REQUIRE(waveMono.getBuffer().countFrames() == prevSize); // size does not change, channels do
+               REQUIRE(waveMono.getBuffer().countChannels() == 2);
+
+               SECTION("test mono->stereo conversion for already stereo wave")
+               {
+                       /* Should do nothing. */
+                       int prevSize = waveStereo.getBuffer().countFrames();
+
+                       REQUIRE(wfx::monoToStereo(waveStereo) == G_RES_OK);
+                       REQUIRE(waveStereo.getBuffer().countFrames() == prevSize);
+                       REQUIRE(waveStereo.getBuffer().countChannels() == 2);
+               }
+       }
+
+       SECTION("test silence")
+       {
+               int a = 20;
+               int b = 57;
+               wfx::silence(waveStereo, a, b);
+
+               for (int i = a; i < b; i++)
+                       for (int k = 0; k < waveStereo.getBuffer().countChannels(); k++)
+                               REQUIRE(waveStereo.getBuffer()[i][k] == 0.0f);
+       }
+
+       SECTION("test cut")
+       {
+               int a        = 47;
+               int b        = 210;
+               int range    = b - a;
+               int prevSize = waveStereo.getBuffer().countFrames();
+
+               wfx::cut(waveStereo, a, b);
+
+               REQUIRE(waveStereo.getBuffer().countFrames() == prevSize - range);
+       }
+
+       SECTION("test trim")
+       {
+               int a    = 47;
+               int b    = 210;
+               int area = b - a;
+
+               wfx::trim(waveStereo, a, b);
+
+               REQUIRE(waveStereo.getBuffer().countFrames() == area);
+       }
+
+       SECTION("test fade")
+       {
+               int a = 47;
+               int b = 500;
+
+               wfx::fade(waveStereo, a, b, wfx::Fade::IN);
+               wfx::fade(waveStereo, a, b, wfx::Fade::OUT);
+
+               REQUIRE(waveStereo.getBuffer()[a][0] == 0.0f);
+               REQUIRE(waveStereo.getBuffer()[a][1] == 0.0f);
+               REQUIRE(waveStereo.getBuffer()[b][0] == 0.0f);
+               REQUIRE(waveStereo.getBuffer()[b][1] == 0.0f);
+       }
+
+       SECTION("test smooth")
+       {
+               int a = 11;
+               int b = 79;
+
+               wfx::smooth(waveStereo, a, b);
+
+               REQUIRE(waveStereo.getBuffer()[a][0] == 0.0f);
+               REQUIRE(waveStereo.getBuffer()[a][1] == 0.0f);
+               REQUIRE(waveStereo.getBuffer()[b][0] == 0.0f);
+               REQUIRE(waveStereo.getBuffer()[b][1] == 0.0f);
+       }
+}
diff --git a/tests/waveManager.cpp b/tests/waveManager.cpp
new file mode 100644 (file)
index 0000000..bd32731
--- /dev/null
@@ -0,0 +1,60 @@
+#include "../src/core/waveManager.h"
+#include "../src/core/const.h"
+#include "../src/core/wave.h"
+#include <catch2/catch.hpp>
+#include <memory>
+#include <samplerate.h>
+
+using std::string;
+using namespace giada::m;
+
+#define G_SAMPLE_RATE 44100
+#define G_BUFFER_SIZE 4096
+#define G_CHANNELS 2
+
+TEST_CASE("waveManager")
+{
+       /* Each SECTION the TEST_CASE is executed from the start. Any code between 
+       this comment and the first SECTION macro is executed before each SECTION. */
+
+       WaveManager waveManager;
+
+       SECTION("test creation")
+       {
+               WaveManager::Result res = waveManager.createFromFile(TEST_RESOURCES_DIR "test.wav",
+                   /*ID=*/0, /*sampleRate=*/G_SAMPLE_RATE, /*quality=*/SRC_LINEAR);
+
+               REQUIRE(res.status == G_RES_OK);
+               REQUIRE(res.wave->getRate() == G_SAMPLE_RATE);
+               REQUIRE(res.wave->getBuffer().countChannels() == G_CHANNELS);
+               REQUIRE(res.wave->isLogical() == false);
+               REQUIRE(res.wave->isEdited() == false);
+       }
+
+       SECTION("test recording")
+       {
+               std::unique_ptr<Wave> wave = waveManager.createEmpty(G_BUFFER_SIZE,
+                   G_MAX_IO_CHANS, G_SAMPLE_RATE, "test.wav");
+
+               REQUIRE(wave->getRate() == G_SAMPLE_RATE);
+               REQUIRE(wave->getBuffer().countFrames() == G_BUFFER_SIZE);
+               REQUIRE(wave->getBuffer().countChannels() == G_CHANNELS);
+               REQUIRE(wave->isLogical() == true);
+               REQUIRE(wave->isEdited() == false);
+       }
+
+       SECTION("test resampling")
+       {
+               WaveManager::Result res = waveManager.createFromFile(TEST_RESOURCES_DIR "test.wav",
+                   /*ID=*/0, /*sampleRate=*/G_SAMPLE_RATE, /*quality=*/SRC_LINEAR);
+
+               int oldSize = res.wave->getBuffer().countFrames();
+               waveManager.resample(*res.wave.get(), 1, G_SAMPLE_RATE * 2);
+
+               REQUIRE(res.wave->getRate() == G_SAMPLE_RATE * 2);
+               REQUIRE(res.wave->getBuffer().countFrames() == oldSize * 2);
+               REQUIRE(res.wave->getBuffer().countChannels() == G_CHANNELS);
+               REQUIRE(res.wave->isLogical() == false);
+               REQUIRE(res.wave->isEdited() == false);
+       }
+}
diff --git a/tests/waveReader.cpp b/tests/waveReader.cpp
new file mode 100644 (file)
index 0000000..ef7b587
--- /dev/null
@@ -0,0 +1,72 @@
+#include "../src/core/channels/waveReader.h"
+#include "../src/core/resampler.h"
+#include "../src/core/wave.h"
+#include "../src/utils/vector.h"
+#include <catch2/catch.hpp>
+#include <memory>
+
+TEST_CASE("WaveReader")
+{
+       using namespace giada;
+
+       constexpr int BUFFER_SIZE  = 1024;
+       constexpr int NUM_CHANNELS = 2;
+
+       m::Wave wave(0);
+       wave.getBuffer().alloc(BUFFER_SIZE, NUM_CHANNELS);
+       wave.getBuffer().forEachFrame([](float* f, int i) {
+               f[0] = static_cast<float>(i + 1);
+               f[1] = static_cast<float>(i + 1);
+       });
+       m::Resampler  resampler;
+       m::WaveReader waveReader(&resampler);
+
+       SECTION("Test initialization")
+       {
+               REQUIRE(waveReader.wave == nullptr);
+       }
+
+       waveReader.wave = &wave;
+
+       SECTION("Test fill, pitch 1.0")
+       {
+               mcl::AudioBuffer out(BUFFER_SIZE, NUM_CHANNELS);
+
+               SECTION("Regular fill")
+               {
+                       m::WaveReader::Result res = waveReader.fill(out,
+                           /*start=*/0, BUFFER_SIZE, /*offset=*/0, /*pitch=*/1.0f);
+
+                       bool allFilled       = true;
+                       int  numFramesFilled = 0;
+                       out.forEachFrame([&allFilled, &numFramesFilled](const float* f, int) {
+                               if (f[0] == 0.0f)
+                                       allFilled = false;
+                               else
+                                       numFramesFilled++;
+                       });
+
+                       REQUIRE(allFilled);
+                       REQUIRE(numFramesFilled == res.used);
+                       REQUIRE(numFramesFilled == res.generated);
+               }
+
+               SECTION("Partial fill")
+               {
+                       m::WaveReader::Result res = waveReader.fill(out,
+                           /*start=*/0, BUFFER_SIZE, /*offset=*/BUFFER_SIZE / 2, /*pitch=*/1.0f);
+
+                       int numFramesFilled = 0;
+                       out.forEachFrame([&numFramesFilled](const float* f, int) {
+                               if (f[0] != 0.0f)
+                                       numFramesFilled++;
+                       });
+
+                       REQUIRE(numFramesFilled == BUFFER_SIZE / 2);
+                       REQUIRE(out[(BUFFER_SIZE / 2) - 1][0] == 0.0f);
+                       REQUIRE(out[BUFFER_SIZE / 2][0] != 0.0f);
+                       REQUIRE(numFramesFilled == res.used);
+                       REQUIRE(numFramesFilled == res.generated);
+               }
+       }
+}