From: Dennis Braun Date: Sun, 15 May 2022 16:50:55 +0000 (+0100) Subject: Import giada_0.21.0.orig.tar.gz X-Git-Tag: archive/raspbian/0.21.0-1+rpi1^2~8 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=ec828633268208c937599c1b928419be29b95784;p=giada.git Import giada_0.21.0.orig.tar.gz [dgit import orig giada_0.21.0.orig.tar.gz] --- ec828633268208c937599c1b928419be29b95784 diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..463096b --- /dev/null +++ b/.clang-format @@ -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 index 0000000..5ec69d1 --- /dev/null +++ b/CMakeLists.txt @@ -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=$> + 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$<$: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 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. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/ChangeLog b/ChangeLog new file mode 100644 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 -. +- 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 index 0000000..f2562da --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +

+ Giada - Your Hardcore Loop Machine +

+ +

+Giada - Your Hardcore Loop Machine | Official website: giadamusic.com | Build status +

+ +## What is Giada? + +Giada is an open source, minimalistic and hardcore music production tool. Designed for DJs, live performers and electronic musicians. + +

+✦✦✦ See Giada in action! ✦✦✦ +

+ +![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 . diff --git a/extras/com.giadamusic.Giada.desktop b/extras/com.giadamusic.Giada.desktop new file mode 100644 index 0000000..b9d33b5 --- /dev/null +++ b/extras/com.giadamusic.Giada.desktop @@ -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 index 0000000..0786ef0 --- /dev/null +++ b/extras/com.giadamusic.Giada.metainfo.xml @@ -0,0 +1,58 @@ + + + + com.giadamusic.Giada + CC0 + GPL-2.0+ + Giada + Your hardcore loop machine + +

+ Giada is an open source, minimalistic and hardcore music + production tool. Designed for DJs, live performers and electronic + musicians. +

+
+ + com.giadamusic.Giada.desktop + + + + https://www.giadamusic.com/images/screenshots/giada-loop-machine-screenshot-03-large-project.png + + A fairly large project with samples and MIDI events + + + + https://www.giadamusic.com/images/screenshots/giada-loop-machine-screenshot-17-midi-action-editor.png + + New Action Editor for MIDI events + + + + https://www.giadamusic.com/images/screenshots/giada-loop-machine-screenshot-01-sample-editor.png + + Chopping samples in the advanced Sample Editor + + + giadaloopmachine_AT_gmail.com + https://www.giadamusic.com/ + https://www.giadamusic.com/forum + + + +
    +
  • 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)
  • +
+
+
+
+
diff --git a/extras/giada-logo.png b/extras/giada-logo.png new file mode 100644 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 index 0000000..17e6885 --- /dev/null +++ b/extras/giada-logo.svg @@ -0,0 +1,17 @@ + + +image/svg+xml diff --git a/extras/giada-logotype.png b/extras/giada-logotype.png new file mode 100644 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 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 index 0000000..aff5570 --- /dev/null +++ b/scripts/create_source_tarball.sh @@ -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 -s " + 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 index 0000000..ba960b8 --- /dev/null +++ b/src/core/actions/action.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..12cdaa4 --- /dev/null +++ b/src/core/actions/actionRecorder.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include +#include + +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(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 actions; + std::unordered_map 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 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 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& 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(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 ActionRecorder::serializeActions(const Actions::Map& actions) +{ + std::vector 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(a1).nextId = a2.id; + const_cast(a2).prevId = a1.id; + + break; + } +} + +/* -------------------------------------------------------------------------- */ + +const std::vector* 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 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 index 0000000..157dc05 --- /dev/null +++ b/src/core/actions/actionRecorder.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_ACTION_RECORDER_H +#define G_ACTION_RECORDER_H + +#include "core/actions/actions.h" +#include "core/midiEvent.h" +#include "core/types.h" +#include +#include + +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 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& as); + std::vector serializeActions(const Actions::Map& as); + + /* Pass-thru functions. See Actions.h */ + + const std::vector* getActionsOnFrame(Frame f) const; + bool hasActions(ID channelId, int type = 0) const; + Action getClosestAction(ID channelId, Frame f, int type) const; + std::vector 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 m_liveActions; +}; +} // namespace giada::m + +#endif diff --git a/src/core/actions/actions.cpp b/src/core/actions/actions.cpp new file mode 100644 index 0000000..04d1dc9 --- /dev/null +++ b/src/core/actions/actions.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "actions.h" +#include "action.h" +#include "core/idManager.h" +#include "core/model/model.h" +#include "utils/log.h" +#include +#include +#include + +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().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 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()) + { + 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() = std::move(temp); +} + +/* -------------------------------------------------------------------------- */ + +void Actions::updateEvent(ID id, MidiEvent e) +{ + model::DataLock lock = m_model.lockData(); + + findAction(m_model.getAllShared(), id)->event = e; +} + +/* -------------------------------------------------------------------------- */ + +void Actions::updateSiblings(ID id, ID prevId, ID nextId) +{ + model::DataLock lock = m_model.lockData(); + + Action* pcurr = findAction(m_model.getAllShared(), id); + Action* pprev = findAction(m_model.getAllShared(), prevId); + Action* pnext = findAction(m_model.getAllShared(), 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()) + 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()[frame].push_back(a); + updateMapPointers(m_model.getAllShared()); + + return a; +} + +/* -------------------------------------------------------------------------- */ + +void Actions::rec(std::vector& actions) +{ + if (actions.size() == 0) + return; + + model::DataLock lock = m_model.lockData(); + + Map& map = m_model.getAllShared(); + + 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[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* Actions::getActionsOnFrame(Frame frame) const +{ + if (m_model.getAllShared().count(frame) == 0) + return nullptr; + return &m_model.getAllShared().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 Actions::getActionsOnChannel(ID channelId) const +{ + std::vector out; + forEachAction([&](const Action& a) { + if (a.channelId == channelId) + out.push_back(a); + }); + return out; +} + +/* -------------------------------------------------------------------------- */ + +void Actions::forEachAction(std::function f) const +{ + for (auto& [_, actions] : m_model.getAllShared()) + 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 f) +{ + model::DataLock lock = m_model.lockData(); + + Map& map = m_model.getAllShared(); + 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()); +} +} // namespace giada::m diff --git a/src/core/actions/actions.h b/src/core/actions/actions.h new file mode 100644 index 0000000..d098eae --- /dev/null +++ b/src/core/actions/actions.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +namespace giada::m::model +{ +class Model; +} + +namespace giada::m +{ +class Actions +{ +public: + using Map = std::map>; + + 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 f) const; + + /* getActionsOnChannel + Returns a vector of actions belonging to channel 'ch'. */ + + std::vector 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* 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 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& 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 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 index 0000000..bdaf232 --- /dev/null +++ b/src/core/channels/audioReceiver.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..eec25bc --- /dev/null +++ b/src/core/channels/audioReceiver.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..7cf7081 --- /dev/null +++ b/src/core/channels/channel.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 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(e.data); + break; + + case EventDispatcher::EventType::CHANNEL_PAN: + pan = std::get(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 index 0000000..020ba9e --- /dev/null +++ b/src/core/channels/channel.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_CHANNEL_H +#define G_CHANNEL_H + +#include +#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 midiQueue; +#endif + + WeakAtomic tracker = 0; + WeakAtomic playStatus = ChannelStatus::OFF; + WeakAtomic recStatus = ChannelStatus::OFF; + WeakAtomic readActions = false; + + std::optional quantizer; + + /* Optional render queue for sample-based channels. Used by SampleReactor + and SampleAdvancer to instruct SamplePlayer how to render audio. */ + + std::optional> 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 = {}; +}; + +/* -------------------------------------------------------------------------- */ + +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 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 plugins; +#endif + + MidiLearner midiLearner; + MidiLighter midiLighter; + + std::optional samplePlayer; + std::optional sampleAdvancer; + std::optional sampleReactor; + std::optional audioReceiver; + std::optional midiController; +#ifdef WITH_VST + std::optional midiReceiver; +#endif + std::optional midiSender; + std::optional sampleActionRecorder; + std::optional 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 index 0000000..51d42e8 --- /dev/null +++ b/src/core/channels/channelManager.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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(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 shared = std::make_unique(bufferSize); + + if (type == ChannelType::SAMPLE || type == ChannelType::PREVIEW) + { + shared->quantizer.emplace(); + shared->renderQueue.emplace(); + shared->resampler.emplace(static_cast(m_conf.rsmpQuality), G_MAX_IO_CHANS); + } + + m_model.addShared(std::move(shared)); + return m_model.backShared(); +} +} // namespace giada::m diff --git a/src/core/channels/channelManager.h b/src/core/channels/channelManager.h new file mode 100644 index 0000000..29b8290 --- /dev/null +++ b/src/core/channels/channelManager.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..0b5286e --- /dev/null +++ b/src/core/channels/midiActionRecorder.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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(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 index 0000000..62f9807 --- /dev/null +++ b/src/core/channels/midiActionRecorder.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..3ad920d --- /dev/null +++ b/src/core/channels/midiController.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "midiController.h" +#include "core/channels/channel.h" +#include "core/conf.h" +#include + +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 index 0000000..61c701e --- /dev/null +++ b/src/core/channels/midiController.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..3c6d92f --- /dev/null +++ b/src/core/channels/midiLearner.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..4bd64a1 --- /dev/null +++ b/src/core/channels/midiLearner.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..d21415f --- /dev/null +++ b/src/core/channels/midiLighter.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/channels/midiLighter.h" +#include "core/kernelMidi.h" +#include "core/midiMapper.h" + +namespace giada::m +{ +template +MidiLighter::MidiLighter(MidiMapper& m) +: enabled(false) +, onSend(nullptr) +, m_midiMapper(&m) +{ +} + +/* -------------------------------------------------------------------------- */ + +template +MidiLighter::MidiLighter(MidiMapper& m, const Patch::Channel& p) +: enabled(p.midiOutL) +, playing(p.midiOutLplaying) +, mute(p.midiOutLmute) +, solo(p.midiOutLsolo) +, m_midiMapper(&m) +{ +} + +/* -------------------------------------------------------------------------- */ + +template +void MidiLighter::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 +void MidiLighter::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 +void MidiLighter::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 +void MidiLighter::send(uint32_t learnt, const MidiMap::Message& msg) +{ + assert(onSend != nullptr); + + m_midiMapper->sendMidiLightning(learnt, msg); + onSend(); +} + +/* -------------------------------------------------------------------------- */ + +template class MidiLighter; +#ifdef WITH_TESTS +template class MidiLighter; +#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 index 0000000..8d540da --- /dev/null +++ b/src/core/channels/midiLighter.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +class MidiLighter final +{ +public: + MidiLighter(MidiMapper&); + MidiLighter(MidiMapper&, 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 onSend; + +private: + void send(uint32_t learnt, const MidiMap::Message&); + + MidiMapper* m_midiMapper; +}; + +extern template class MidiLighter; +#ifdef WITH_TESTS +extern template class MidiLighter; +#endif +} // namespace giada::m + +#endif diff --git a/src/core/channels/midiReceiver.cpp b/src/core/channels/midiReceiver.cpp new file mode 100644 index 0000000..599f2ca --- /dev/null +++ b/src/core/channels/midiReceiver.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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(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 index 0000000..4f8fdb5 --- /dev/null +++ b/src/core/channels/midiReceiver.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..6eee0ba --- /dev/null +++ b/src/core/channels/midiSender.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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& 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 index 0000000..140f291 --- /dev/null +++ b/src/core/channels/midiSender.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 onSend; + +private: + void send(MidiEvent e) const; + void parseActions(const Channel& ch, const std::vector& as) const; +}; +} // namespace giada::m + +#endif diff --git a/src/core/channels/sampleActionRecorder.cpp b/src/core/channels/sampleActionRecorder.cpp new file mode 100644 index 0000000..d03c752 --- /dev/null +++ b/src/core/channels/sampleActionRecorder.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..a780c02 --- /dev/null +++ b/src/core/channels/sampleActionRecorder.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..9f0391a --- /dev/null +++ b/src/core/channels/sampleAdvancer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/channels/sampleAdvancer.h" +#include "core/channels/channel.h" +#include + +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& 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 index 0000000..e3246c0 --- /dev/null +++ b/src/core/channels/sampleAdvancer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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& 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 index 0000000..bf87e55 --- /dev/null +++ b/src/core/channels/samplePlayer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "samplePlayer.h" +#include "core/channels/channel.h" +#include "core/wave.h" +#include "deps/mcl-audio-buffer/src/audioBuffer.hpp" +#include +#include + +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(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 index 0000000..f6256ab --- /dev/null +++ b/src/core/channels/samplePlayer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 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 index 0000000..b009d7b --- /dev/null +++ b/src/core/channels/sampleReactor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/channels/sampleReactor.h" +#include "core/channels/channel.h" +#include "core/conf.h" +#include "core/model/model.h" +#include "utils/math.h" +#include + +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(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 index 0000000..0915779 --- /dev/null +++ b/src/core/channels/sampleReactor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..bbc206d --- /dev/null +++ b/src/core/channels/waveReader.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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(res.used), + static_cast(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 index 0000000..d12dfff --- /dev/null +++ b/src/core/channels/waveReader.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..ed38bf8 --- /dev/null +++ b/src/core/conf.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +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(data.recTriggerMode); + j[CONF_KEY_REC_TRIGGER_LEVEL] = data.recTriggerLevel; + j[CONF_KEY_INPUT_REC_MODE] = static_cast(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 index 0000000..8541878 --- /dev/null +++ b/src/core/conf.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_CONF_H +#define G_CONF_H + +#include "core/const.h" +#include "core/types.h" +#include "utils/gui.h" +#include +#include + +namespace giada::m +{ +class Conf final +{ +public: + using KeyBindings = std::unordered_map; + + 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 index 0000000..db9bc5b --- /dev/null +++ b/src/core/const.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_CONST_H +#define G_CONST_H + +#include +#include + +/* -- 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(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(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 index 0000000..2b05d8f --- /dev/null +++ b/src/core/engine.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/engine.h" +#include "core/model/model.h" +#include "core/model/storage.h" +#include "utils/fs.h" +#include "utils/log.h" +#include + +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(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(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(kernelInfo.outBuf), kernelInfo.bufferSize, kernelInfo.channelsOutCount); + mcl::AudioBuffer in; + if (kernelInfo.channelsInCount > 0) + in = mcl::AudioBuffer(static_cast(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 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 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& w : model.getAllShared()) + { + w->setPath(makeUniqueWavePath(projectPath, *w, model.getAllShared())); + 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 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 index 0000000..c7d242e --- /dev/null +++ b/src/core/engine.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 missingWaves = {}; + std::vector 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 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 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 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 index 0000000..e0c8939 --- /dev/null +++ b/src/core/eventDispatcher.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/eventDispatcher.h" +#include "core/const.h" +#include + +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(e.data).event); + break; + + case EventType::MIDI_DISPATCHER_PROCESS: + onMidiProcess(std::get(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 index 0000000..3462201 --- /dev/null +++ b/src/core/eventDispatcher.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +/* 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; + + 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; + + 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 UIevents; + Queue MidiEvents; + + /* on[...] + Callbacks fired when something happens in the Event Dispatcher. */ + + std::function onMidiLearn; + std::function onMidiProcess; + std::function onProcessChannels; + std::function onProcessSequencer; + std::function onMixerSignalCallback; + std::function 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 index 0000000..10eb1b0 --- /dev/null +++ b/src/core/graphics.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..7e18e5f --- /dev/null +++ b/src/core/graphics.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..87f62bb --- /dev/null +++ b/src/core/idManager.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..32bf1ae --- /dev/null +++ b/src/core/idManager.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..ccd97f0 --- /dev/null +++ b/src/core/init.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifdef __APPLE__ +#include +#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 +#include +#include +#endif +#include + +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 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 index 0000000..1c587e2 --- /dev/null +++ b/src/core/init.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..9ebcac6 --- /dev/null +++ b/src/core/jackTransport.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "jackTransport.h" +#ifdef WITH_AUDIO_JACK +#include +#include +#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 index 0000000..b733474 --- /dev/null +++ b/src/core/jackTransport.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_JACK_TRANSPORT_H +#define G_JACK_TRANSPORT_H + +#ifdef WITH_AUDIO_JACK +#include +#endif +#include + +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 index 0000000..4fa059f --- /dev/null +++ b/src/core/kernelAudio.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/kernelAudio.h" +#include "core/conf.h" +#include "core/const.h" +#include "utils/log.h" +#include "utils/vector.h" +#include +#include + +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::UNIX_JACK); + else if (m_api == G_SYS_API_ALSA && hasAPI(RtAudio::LINUX_ALSA)) + m_rtAudio = std::make_unique(RtAudio::LINUX_ALSA); + else if (m_api == G_SYS_API_PULSE && hasAPI(RtAudio::LINUX_PULSE)) + m_rtAudio = std::make_unique(RtAudio::LINUX_PULSE); + +#elif defined(__FreeBSD__) + + if (m_api == G_SYS_API_JACK && hasAPI(RtAudio::UNIX_JACK)) + m_rtAudio = std::make_unique(RtAudio::UNIX_JACK); + else if (m_api == G_SYS_API_PULSE && hasAPI(RtAudio::LINUX_PULSE)) + m_rtAudio = std::make_unique(RtAudio::LINUX_PULSE); + +#elif defined(_WIN32) + + if (m_api == G_SYS_API_DS && hasAPI(RtAudio::WINDOWS_DS)) + m_rtAudio = std::make_unique(RtAudio::WINDOWS_DS); + else if (m_api == G_SYS_API_ASIO && hasAPI(RtAudio::WINDOWS_ASIO)) + m_rtAudio = std::make_unique(RtAudio::WINDOWS_ASIO); + else if (m_api == G_SYS_API_WASAPI && hasAPI(RtAudio::WINDOWS_WASAPI)) + m_rtAudio = std::make_unique(RtAudio::WINDOWS_WASAPI); + +#elif defined(__APPLE__) + + if (m_api == G_SYS_API_CORE && hasAPI(RtAudio::MACOSX_CORE)) + m_rtAudio = std::make_unique(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(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& KernelAudio::getDevices() const +{ + return m_devices; +} + +/* -------------------------------------------------------------------------- */ + +#ifdef WITH_AUDIO_JACK +jack_client_t* KernelAudio::getJackHandle() const +{ + return static_cast(m_rtAudio->HACK__getJackClient()); +} +#endif + +/* -------------------------------------------------------------------------- */ + +bool KernelAudio::hasAPI(int API) const +{ + std::vector 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 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(info.outputChannels), + static_cast(info.inputChannels), + static_cast(info.duplexChannels), + info.isDefaultOutput, + info.isDefaultInput, + u::vector::cast(info.sampleRates)}; +} + +/* -------------------------------------------------------------------------- */ + +std::vector KernelAudio::fetchDevices() const +{ + std::vector out; + for (unsigned i = 0; i < m_rtAudio->getDeviceCount(); i++) + out.push_back(fetchDevice(i)); + return out; +} + +/* -------------------------------------------------------------------------- */ + +void KernelAudio::printDevices(const std::vector& 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(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 index 0000000..e509e91 --- /dev/null +++ b/src/core/kernelAudio.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_KERNELAUDIO_H +#define G_KERNELAUDIO_H + +#include "core/conf.h" +#include "deps/rtaudio/RtAudio.h" +#include +#include +#include +#include +#include +#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 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& getDevices() const; +#ifdef WITH_AUDIO_JACK + jack_client_t* getJackHandle() const; +#endif + + /* onAudioCallback + Main callback invoked on each audio block. */ + + std::function onAudioCallback; + +private: + static int audioCallback(void*, void*, unsigned, double, RtAudioStreamStatus, void*); + + Device fetchDevice(size_t deviceIndex) const; + std::vector fetchDevices() const; + void printDevices(const std::vector& devices) const; + +#ifdef WITH_AUDIO_JACK + JackTransport m_jackTransport; +#endif + std::vector m_devices; + std::unique_ptr 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 index 0000000..040b0e2 --- /dev/null +++ b/src/core/kernelMidi.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/kernelMidi.h" +#include "core/const.h" +#include "utils/log.h" +#include +#include + +namespace giada::m +{ +namespace +{ +constexpr auto OUTPUT_NAME = "Giada MIDI output"; +constexpr auto INPUT_NAME = "Giada MIDI input"; + +/* -------------------------------------------------------------------------- */ + +std::vector split_(uint32_t iValue) +{ + return { + static_cast((iValue >> 24) & 0xFF), + static_cast((iValue >> 16) & 0xFF), + static_cast((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(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(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 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 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 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* msg, void* data) +{ + static_cast(data)->callback(msg); +} + +/* -------------------------------------------------------------------------- */ + +void KernelMidi::callback(std::vector* 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 +std::unique_ptr KernelMidi::makeDevice(int api, std::string name) const +{ + try + { + return std::make_unique(static_cast(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 KernelMidi::makeDevice(int, std::string) const; +template std::unique_ptr 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 index 0000000..ac10e22 --- /dev/null +++ b/src/core/kernelMidi.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_KERNELMIDI_H +#define G_KERNELMIDI_H + +#include "midiMapper.h" +#include +#include +#include +#include +#include + +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 onMidiReceived; + +private: + static void s_callback(double, std::vector*, void*); + void callback(std::vector*); + + template + std::unique_ptr 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 m_midiOut; + std::unique_ptr m_midiIn; +}; +} // namespace giada::m + +#endif diff --git a/src/core/metronome.cpp b/src/core/metronome.cpp new file mode 100644 index 0000000..7b5fbb0 --- /dev/null +++ b/src/core/metronome.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..01bee57 --- /dev/null +++ b/src/core/metronome.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..2cff699 --- /dev/null +++ b/src/core/midiDispatcher.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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 f) +{ + m_learnCb = [=](m::MidiEvent e) { learnChannel(e, param, channelId, f); }; +} + +void MidiDispatcher::startMasterLearn(int param, std::function f) +{ + m_learnCb = [=](m::MidiEvent e) { learnMaster(e, param, f); }; +} + +#ifdef WITH_VST + +void MidiDispatcher::startPluginLearn(std::size_t paramIndex, ID pluginId, std::function 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 f) +{ + learnMaster(MidiEvent(), param, f); // Empty event (0x0) +} + +void MidiDispatcher::clearChannelLearn(int param, ID channelId, std::function f) +{ + learnChannel(MidiEvent(), param, channelId, f); // Empty event (0x0) +} + +#ifdef WITH_VST + +void MidiDispatcher::clearPluginLearn(std::size_t paramIndex, ID pluginId, std::function 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& 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 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 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 doneCb) +{ + model::DataLock lock = m_model.lockData(model::SwapType::NONE); + Plugin* plugin = m_model.findShared(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 index 0000000..8bef6a4 --- /dev/null +++ b/src/core/midiDispatcher.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +namespace giada::m +{ +class MidiDispatcher +{ +public: + MidiDispatcher(model::Model&); + + void startChannelLearn(int param, ID channelId, std::function f); + void startMasterLearn(int param, std::function f); + void stopLearn(); + void clearMasterLearn(int param, std::function f); + void clearChannelLearn(int param, ID channelId, std::function f); +#ifdef WITH_VST + void startPluginLearn(std::size_t paramIndex, ID pluginId, std::function f); + void clearPluginLearn(std::size_t paramIndex, ID pluginId, std::function 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 onDispatch; + + /* onEventReceived + Callback fired when a MIDI event has been received (passed in by the Event + Dispatcher). */ + + std::function 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 doneCb); + void learnMaster(MidiEvent e, int param, std::function doneCb); + +#ifdef WITH_VST + void processPlugins(ID channelId, const std::vector& plugins, + const MidiEvent& midiEvent); + void learnPlugin(MidiEvent e, std::size_t paramIndex, ID pluginId, + std::function 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 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 index 0000000..761106a --- /dev/null +++ b/src/core/midiEvent.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "midiEvent.h" +#include "const.h" +#include "utils/math.h" +#include + +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((byte1 << 24) | (byte2 << 16) | (byte3 << 8) | (0x00)), delta) +{ +} + +/* -------------------------------------------------------------------------- */ + +MidiEvent::MidiEvent(float v, int delta) +: MidiEvent(ENVELOPE, 0, 0, delta) +{ + m_velocity = static_cast(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_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 index 0000000..15ff0e7 --- /dev/null +++ b/src/core/midiEvent.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_MIDI_EVENT_H +#define G_MIDI_EVENT_H + +#include + +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 index 0000000..8166aee --- /dev/null +++ b/src/core/midiLearnParam.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "midiLearnParam.h" +#include + +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 index 0000000..bf51f80 --- /dev/null +++ b/src/core/midiLearnParam.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_MIDI_LEARN_PARAM_H +#define G_MIDI_LEARN_PARAM_H + +#include "core/weakAtomic.h" +#include +#include + +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 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 index 0000000..839d69d --- /dev/null +++ b/src/core/midiMapper.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include +#include +#include +#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 +MidiMapper::MidiMapper(KernelMidiI& k) +: m_kernelMidi(k) +{ +} + +/* -------------------------------------------------------------------------- */ + +template +const std::vector& MidiMapper::getMapFilesFound() const +{ + return m_mapFiles; +} + +/* -------------------------------------------------------------------------- */ + +template +void MidiMapper::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 +int MidiMapper::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 +bool MidiMapper::isMessageDefined(const MidiMap::Message& m) const +{ + return m.offset != -1; +} + +/* -------------------------------------------------------------------------- */ + +template +void MidiMapper::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 +void MidiMapper::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 +void MidiMapper::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 +bool MidiMapper::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 +bool MidiMapper::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; +#ifdef WITH_TESTS +template class MidiMapper; +#endif +} // namespace giada::m diff --git a/src/core/midiMapper.h b/src/core/midiMapper.h new file mode 100644 index 0000000..f8f2c13 --- /dev/null +++ b/src/core/midiMapper.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_MIDIMAPPER_H +#define G_MIDIMAPPER_H + +#include "deps/json/single_include/nlohmann/json.hpp" +#include +#include + +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 initCommands; + Message muteOn; + Message muteOff; + Message soloOn; + Message soloOff; + Message waiting; + Message playing; + Message stopping; + Message stopped; + Message playingInaudible; +}; + +template +class MidiMapper final +{ +public: + MidiMapper(KernelMidiI&); + + /* getMapFilesFound + Returns a reference to the list of midimaps found. */ + + const std::vector& 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 m_mapFiles; +}; + +extern template class MidiMapper; +#ifdef WITH_TESTS +extern template class MidiMapper; +#endif +} // namespace giada::m + +#endif diff --git a/src/core/mixer.cpp b/src/core/mixer.cpp new file mode 100644 index 0000000..bd49e43 --- /dev/null +++ b/src/core/mixer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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& 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 index 0000000..d10c933 --- /dev/null +++ b/src/core/mixer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 quantizerStep); + + /* onSignalTresholdReached + Callback fired when audio has reached a certain threshold (record-on-signal + mode). */ + + std::function onSignalTresholdReached; + + /* onEndOfRecording + Callback fired when the audio recording session has ended. */ + + std::function 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& 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 index 0000000..af88cf2 --- /dev/null +++ b/src/core/mixerHandler.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +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 w) +{ + assert(onChannelsAltered != nullptr); + + m_model.addShared(std::move(w)); + + Channel& channel = m_model.get().getChannel(channelId); + Wave& wave = m_model.backShared(); + 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(*old); + + onChannelsAltered(); +} + +/* -------------------------------------------------------------------------- */ + +void MixerHandler::addAndLoadChannel(ID columnId, std::unique_ptr w, int bufferSize, + ChannelManager& channelManager) +{ + assert(onChannelsAltered != nullptr); + + m_model.addShared(std::move(w)); + + Wave& wave = m_model.backShared(); + 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()); + } + +#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 newPlugins; + for (const Plugin* plugin : oldChannel.plugins) + { + m_model.addShared(pluginManager.makePlugin(*plugin, sampleRate, bufferSize, sequencer)); + newPlugins.push_back(&m_model.backShared()); + } + 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); + + 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(); + + 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 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); + + 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 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 MixerHandler::getChannelsIf(std::function f) +{ + std::vector out; + for (Channel& ch : m_model.get().channels) + if (f(ch)) + out.push_back(&ch); + return out; +} + +std::vector MixerHandler::getRecordableChannels() +{ + return getChannelsIf([](const Channel& c) { return c.canInputRec() && !c.hasWave(); }); +} + +std::vector 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 = 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()); + 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 index 0000000..65364c9 --- /dev/null +++ b/src/core/mixerHandler.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_MIXER_HANDLER_H +#define G_MIXER_HANDLER_H + +#include "core/plugins/pluginManager.h" +#include "core/waveManager.h" +#include "types.h" +#include +#include +#include + +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 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 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 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(Frame)> onChannelRecorded; + +private: + bool forAnyChannel(std::function f) const; + + void loadChannel(Channel&, Wave*) const; + + std::vector getChannelsIf(std::function f); + std::vector getRecordableChannels(); + std::vector 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 index 0000000..7ed5dd7 --- /dev/null +++ b/src/core/model/mixer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..a5a7499 --- /dev/null +++ b/src/core/model/mixer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 active = false; + WeakAtomic peakOutL = 0.0f; + WeakAtomic peakOutR = 0.0f; + WeakAtomic peakInL = 0.0f; + WeakAtomic peakInR = 0.0f; + WeakAtomic 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 index 0000000..f7b7daa --- /dev/null +++ b/src/core/model/model.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/model/model.h" +#include +#include +#ifdef G_DEBUG_MODE +#include "core/channels/channelManager.h" +#endif + +using namespace mcl; + +namespace giada::m::model +{ +namespace +{ +template +auto getIter_(const std::vector>& source, ID id) +{ + return u::vector::findIf(source, [id](const std::unique_ptr& p) { return p->id == id; }); +} + +/* -------------------------------------------------------------------------- */ + +template +auto* get_(S& source, ID id) +{ + auto it = getIter_(source, id); + return it == source.end() ? nullptr : it->get(); +} + +/* -------------------------------------------------------------------------- */ + +template +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(const_cast(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 +T& Model::getAllShared() +{ +#ifdef WITH_VST + if constexpr (std::is_same_v) + return m_shared.plugins; +#endif + if constexpr (std::is_same_v) + return m_shared.waves; + if constexpr (std::is_same_v) + return m_shared.actions; + if constexpr (std::is_same_v) + return m_shared.channelsShared; + + assert(false); +} + +#ifdef WITH_VST +template PluginPtrs& Model::getAllShared(); +#endif +template WavePtrs& Model::getAllShared(); +template Actions::Map& Model::getAllShared(); +template ChannelSharedPtrs& Model::getAllShared(); + +/* -------------------------------------------------------------------------- */ + +template +T* Model::findShared(ID id) +{ +#ifdef WITH_VST + if constexpr (std::is_same_v) + return get_(m_shared.plugins, id); +#endif + if constexpr (std::is_same_v) + return get_(m_shared.waves, id); + + assert(false); +} + +#ifdef WITH_VST +template Plugin* Model::findShared(ID id); +#endif +template Wave* Model::findShared(ID id); + +/* -------------------------------------------------------------------------- */ + +template +void Model::addShared(T obj) +{ +#ifdef WITH_VST + if constexpr (std::is_same_v) + m_shared.plugins.push_back(std::move(obj)); +#endif + if constexpr (std::is_same_v) + m_shared.waves.push_back(std::move(obj)); + if constexpr (std::is_same_v) + m_shared.channelsShared.push_back(std::move(obj)); +} + +#ifdef WITH_VST +template void Model::addShared(PluginPtr p); +#endif +template void Model::addShared(WavePtr p); +template void Model::addShared(ChannelSharedPtr p); + +/* -------------------------------------------------------------------------- */ + +template +void Model::removeShared(const T& ref) +{ +#ifdef WITH_VST + if constexpr (std::is_same_v) + remove_(m_shared.plugins, ref); +#endif + if constexpr (std::is_same_v) + remove_(m_shared.waves, ref); +} + +#ifdef WITH_VST +template void Model::removeShared(const Plugin& t); +#endif +template void Model::removeShared(const Wave& t); + +/* -------------------------------------------------------------------------- */ + +template +T& Model::backShared() +{ +#ifdef WITH_VST + if constexpr (std::is_same_v) + return *m_shared.plugins.back().get(); +#endif + if constexpr (std::is_same_v) + return *m_shared.waves.back().get(); + if constexpr (std::is_same_v) + return *m_shared.channelsShared.back().get(); +} + +#ifdef WITH_VST +template Plugin& Model::backShared(); +#endif +template Wave& Model::backShared(); +template ChannelShared& Model::backShared(); + +/* -------------------------------------------------------------------------- */ + +template +void Model::clearShared() +{ +#ifdef WITH_VST + if constexpr (std::is_same_v) + m_shared.plugins.clear(); +#endif + if constexpr (std::is_same_v) + m_shared.waves.clear(); +} + +#ifdef WITH_VST +template void Model::clearShared(); +#endif +template void Model::clearShared(); + +/* -------------------------------------------------------------------------- */ + +#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()) + { + 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 index 0000000..28b7c6b --- /dev/null +++ b/src/core/model/model.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 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::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; +#endif +using WavePtr = std::unique_ptr; +using ChannelSharedPtr = std::unique_ptr; + +#ifdef WITH_VST +using PluginPtrs = std::vector; +#endif +using WavePtrs = std::vector; +using ChannelSharedPtrs = std::vector; + +/* -------------------------------------------------------------------------- */ + +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 + T& getAllShared(); + + /* findShared + Finds something in the shared data given an ID. Returns nullptr if the + object is not found. */ + + template + T* findShared(ID id); + + /* addShared + Adds some shared data (by moving it). */ + + template + void addShared(T); + + template + void removeShared(const T&); + + /* backShared + Returns a reference to the last added shared item. */ + + template + T& backShared(); + + template + 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 onSwap = nullptr; + +private: + struct Shared + { + Sequencer::Shared sequencerShared; + Mixer::Shared mixerShared; + Recorder::Shared recorderShared; + std::vector> channelsShared; + + std::vector> waves; + Actions::Map actions; +#ifdef WITH_VST + std::vector> plugins; +#endif + }; + + mcl::AtomicSwapper 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 index 0000000..5d714fe --- /dev/null +++ b/src/core/model/recorder.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..aca24e9 --- /dev/null +++ b/src/core/model/recorder.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 isRecordingAction = false; + WeakAtomic 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 index 0000000..426f9d7 --- /dev/null +++ b/src/core/model/sequencer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..5389e86 --- /dev/null +++ b/src/core/model/sequencer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 currentFrame = 0; + WeakAtomic 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 index 0000000..1065b91 --- /dev/null +++ b/src/core/model/storage.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +extern giada::m::Engine g_engine; + +namespace giada::m::model +{ +namespace +{ +void loadChannels_(const std::vector& channels, int samplerate) +{ + float samplerateRatio = g_engine.kernelAudio.getSampleRate() / static_cast(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& pactions) +{ + g_engine.model.getAllShared() = 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()) + patch.plugins.push_back(g_engine.pluginManager.serializePlugin(*p)); +#endif + + patch.actions = g_engine.actionRecorder.serializeActions(g_engine.model.getAllShared()); + + patch.waves.clear(); + for (const auto& w : g_engine.model.getAllShared()) + 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().clear(); + + LoadState state; + + /* Load external data first: plug-ins and waves. */ + +#ifdef WITH_VST + g_engine.model.getAllShared().clear(); + for (const Patch::Plugin& pplugin : patch.plugins) + { + std::unique_ptr 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().push_back(std::move(p)); + } +#endif + + g_engine.model.getAllShared().clear(); + for (const Patch::Wave& pwave : patch.waves) + { + std::unique_ptr w = g_engine.waveManager.deserializeWave(pwave, g_engine.kernelAudio.getSampleRate(), + g_engine.conf.data.rsmpQuality); + + if (w != nullptr) + g_engine.model.getAllShared().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 index 0000000..aa1ce01 --- /dev/null +++ b/src/core/model/storage.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..498b12f --- /dev/null +++ b/src/core/patch.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "patch.h" +#include "core/mixer.h" +#include "deps/json/single_include/nlohmann/json.hpp" +#include "utils/log.h" +#include "utils/math.h" +#include + +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(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(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(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(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(j[PATCH_KEY_VERSION_MAJOR]), + static_cast(j[PATCH_KEY_VERSION_MINOR]), + static_cast(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 index 0000000..f359251 --- /dev/null +++ b/src/core/patch.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_PATCH_H +#define G_PATCH_H + +#include "core/const.h" +#include "core/types.h" +#include +#include +#include + +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 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 params; // TODO - to be removed in 0.18.0 + std::string state; + std::vector 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 columns; + std::vector channels; + std::vector actions; + std::vector waves; +#ifdef WITH_VST + std::vector 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 index 0000000..23e1dde --- /dev/null +++ b/src/core/plugins/plugin.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifdef WITH_VST + +#include "core/plugins/plugin.h" +#include "core/const.h" +#include "utils/log.h" +#include "utils/time.h" +#include +#include +#include + +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 plugin, + std::unique_ptr 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(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(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 index 0000000..0609327 --- /dev/null +++ b/src/core/plugins/plugin.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +namespace giada::m +{ +class Plugin : private juce::ComponentListener +{ +public: + using Buffer = juce::AudioBuffer; + + /* 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, std::unique_ptr, + 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 midiInParams; + + /* valid + A missing plug-in is loaded anyway, yet marked as 'invalid'. */ + + bool valid; + + std::function 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 m_plugin; + std::unique_ptr m_playHead; + Buffer m_buffer; + + std::atomic 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 index 0000000..4d8c724 --- /dev/null +++ b/src/core/plugins/pluginHost.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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& 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 p) +{ + m_model.addShared(std::move(p)); + return m_model.backShared(); +} + +/* -------------------------------------------------------------------------- */ + +void PluginHost::swapPlugin(const m::Plugin& p1, const m::Plugin& p2, std::vector& 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& plugins) +{ + for (const Plugin* p : plugins) + m_model.removeShared(*p); +} + +/* -------------------------------------------------------------------------- */ + +void PluginHost::freeAllPlugins() +{ + m_model.clearShared(); +} + +/* -------------------------------------------------------------------------- */ + +void PluginHost::setPluginParameter(ID pluginId, int paramIndex, float value) +{ + m_model.findShared(pluginId)->setParameter(paramIndex, value); +} + +/* -------------------------------------------------------------------------- */ + +void PluginHost::setPluginProgram(ID pluginId, int programIndex) +{ + m_model.findShared(pluginId)->setCurrentProgram(programIndex); +} + +/* -------------------------------------------------------------------------- */ + +void PluginHost::toggleBypass(ID pluginId) +{ + Plugin& plugin = *m_model.findShared(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::deinterleaveSamples( + AudioData::InterleavedSource{outBuf[0], outBuf.countChannels()}, + AudioData::NonInterleavedDest{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::interleaveSamples( + AudioData::NonInterleavedSource{m_audioBuffer.getArrayOfReadPointers(), m_audioBuffer.getNumChannels()}, + AudioData::InterleavedDest{outBuf[0], outBuf.countChannels()}, + outBuf.countFrames()); +} + +/* -------------------------------------------------------------------------- */ + +void PluginHost::processPlugins(const std::vector& 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 index 0000000..2663690 --- /dev/null +++ b/src/core/plugins/pluginHost.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifdef WITH_VST + +#ifndef G_PLUGIN_HOST_H +#define G_PLUGIN_HOST_H + +#include "core/types.h" +#include "deps/juce-config.h" +#include +#include + +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 p); + + /* processStack + Applies the fx list to the buffer. */ + + void processStack(mcl::AudioBuffer& outBuf, const std::vector& 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& 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& 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&, juce::MidiBuffer& events); + + void processPlugin(Plugin*, const juce::MidiBuffer& events); + + model::Model& m_model; + + juce::AudioBuffer 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 index 0000000..8ffbf6e --- /dev/null +++ b/src/core/plugins/pluginManager.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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& 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 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 elem(juce::XmlDocument::parse(juce::File(filepath))); + if (elem == nullptr) + return false; + m_knownPluginList.recreateFromXml(*elem); + return true; +} + +/* -------------------------------------------------------------------------- */ + +std::unique_ptr 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 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 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( + m_pluginId.generate(id), + std::move(pi), + std::make_unique(sequencer, sampleRate), + sampleRate, bufferSize); +} + +/* -------------------------------------------------------------------------- */ + +std::unique_ptr 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 PluginManager::makePlugin(const Plugin& src, int sampleRate, + int bufferSize, const Sequencer& sequencer) +{ + std::unique_ptr 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 PluginManager::deserializePlugin(const Patch::Plugin& p, + int sampleRate, int bufferSize, const Sequencer& sequencer) +{ + std::unique_ptr 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 PluginManager::hydratePlugins(std::vector pluginIds, model::Model& model) +{ + std::vector out; + for (ID id : pluginIds) + { + Plugin* plugin = model.findShared(id); + if (plugin != nullptr) + out.push_back(plugin); + } + return out; +} + +/* -------------------------------------------------------------------------- */ + +int PluginManager::countAvailablePlugins() const +{ + return m_knownPluginList.getNumTypes(); +} + +/* -------------------------------------------------------------------------- */ + +std::vector PluginManager::getPluginsInfo() const +{ + std::vector 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 PluginManager::makeInvalidPlugin(const std::string& pid, ID id) +{ + m_missingPlugins = true; + m_unknownPluginList.push_back(pid); + return std::make_unique(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 index 0000000..6e6b58a --- /dev/null +++ b/src/core/plugins/pluginManager.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 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& 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 makePlugin(const std::string& pid, int sampleRate, int bufferSize, const Sequencer&, ID id = 0); + std::unique_ptr makePlugin(int index, int sampleRate, int bufferSize, const Sequencer&); + std::unique_ptr 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 deserializePlugin(const Patch::Plugin&, int sampleRate, int bufferSize, const Sequencer&); + std::vector hydratePlugins(std::vector pluginIds, model::Model& model); + + void sortPlugins(SortMethod sortMethod); + +private: + std::unique_ptr 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 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 index 0000000..17cb8f1 --- /dev/null +++ b/src/core/plugins/pluginState.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifdef WITH_VST + +#include "pluginState.h" +#include "core/const.h" +#include + +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 index 0000000..753e66d --- /dev/null +++ b/src/core/plugins/pluginState.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifdef WITH_VST + +#ifndef G_PLUGIN_STATE_H +#define G_PLUGIN_STATE_H + +#include "deps/juce-config.h" +#include +#include + +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 index 0000000..224700e --- /dev/null +++ b/src/core/quantizer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "quantizer.h" +#include + +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 f) +{ + m_callbacks[id] = f; +} + +/* -------------------------------------------------------------------------- */ + +void Quantizer::advance(Range 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 index 0000000..c2f9070 --- /dev/null +++ b/src/core/quantizer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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); + + /* 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 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> m_callbacks; + WeakAtomic m_performId = -1; +}; +} // namespace giada::m + +#endif diff --git a/src/core/queue.h b/src/core/queue.h new file mode 100644 index 0000000..811a50d --- /dev/null +++ b/src/core/queue.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_QUEUE_H +#define G_QUEUE_H + +#include +#include +#include + +namespace giada::m +{ +/* Queue +Single producer, single consumer lock-free queue. */ + +template +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 m_data; + std::atomic m_head; + std::atomic m_tail; +}; +} // namespace giada::m + +#endif diff --git a/src/core/range.h b/src/core/range.h new file mode 100644 index 0000000..511dd1a --- /dev/null +++ b/src/core/range.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_RANGE_H +#define G_RANGE_H + +#include + +namespace giada +{ +template +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 index 0000000..19165cc --- /dev/null +++ b/src/core/recorder.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..34e4c4c --- /dev/null +++ b/src/core/recorder.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..4d22726 --- /dev/null +++ b/src/core/resampler.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/resampler.h" +#include +#include +#include +#include + +#include + +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(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(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 index 0000000..2f563b6 --- /dev/null +++ b/src/core/resampler.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_RESAMPLER_H +#define G_RESAMPLER_H + +#include +#include + +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 index 0000000..04690a5 --- /dev/null +++ b/src/core/ringBuffer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_RING_BUFFER_H +#define G_RING_BUFFER_H + +#include +#include + +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 +class RingBuffer +{ +public: + using iterator = typename std::array::iterator; + using const_iterator = typename std::array::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 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 index 0000000..f479a51 --- /dev/null +++ b/src/core/sequencer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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(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(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(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* 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(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((sampleRate * (60.0f / s.bpm)) * s.beats); + s.framesInBar = static_cast(s.framesInLoop / (float)s.bars); + s.framesInBeat = static_cast(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 index 0000000..97f53d2 --- /dev/null +++ b/src/core/sequencer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_SEQUENCER_H +#define G_SEQUENCER_H + +#include "core/eventDispatcher.h" +#include "core/metronome.h" +#include "core/quantizer.h" +#include + +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* actions = nullptr; + }; + + using EventBuffer = RingBuffer; + + 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 onAboutStart; + std::function onAboutStop; + std::function 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 index 0000000..504806f --- /dev/null +++ b/src/core/synchronizer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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((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 index 0000000..2251847 --- /dev/null +++ b/src/core/synchronizer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 onJackRewind; + std::function onJackChangeBpm; + std::function onJackStart; + std::function 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 index 0000000..f19b6a4 --- /dev/null +++ b/src/core/types.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..264f1d7 --- /dev/null +++ b/src/core/wave.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "wave.h" +#include "const.h" +#include "utils/fs.h" +#include "utils/log.h" +#include "utils/string.h" +#include + +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 index 0000000..2e94af9 --- /dev/null +++ b/src/core/wave.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_WAVE_H +#define G_WAVE_H + +#include "core/types.h" +#include "deps/mcl-audio-buffer/src/audioBuffer.hpp" +#include + +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 index 0000000..7f5aa7e --- /dev/null +++ b/src/core/waveFx.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "waveFx.h" +#include "const.h" +#include "deps/mcl-audio-buffer/src/audioBuffer.hpp" +#include "utils/log.h" +#include "wave.h" +#include +#include +#include + +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 index 0000000..37bf462 --- /dev/null +++ b/src/core/waveFx.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..fbb16bb --- /dev/null +++ b/src/core/waveManager.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +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>& 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>& 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 = std::make_unique(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 WaveManager::createEmpty(int frames, int channels, int samplerate, + const std::string& name) +{ + std::unique_ptr wave = std::make_unique(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 WaveManager::createFromWave(const Wave& src, int a, int b) +{ + int channels = src.getBuffer().countChannels(); + int frames = b - a; + + std::unique_ptr wave = std::make_unique(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 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(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 index 0000000..a50f8ec --- /dev/null +++ b/src/core/waveManager.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +namespace giada::m +{ +std::string makeUniqueWavePath(const std::string& base, const m::Wave& w, + const std::vector>& waves); + +/* -------------------------------------------------------------------------- */ + +class WaveManager final +{ +public: + struct Result + { + int status; + std::unique_ptr 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 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 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 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 index 0000000..f2bca23 --- /dev/null +++ b/src/core/weakAtomic.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_WEAK_ATOMIC_H +#define G_WEAK_ATOMIC_H + +#include +#include + +namespace giada +{ +template +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 onChange = nullptr; + +private: + std::atomic 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 index 0000000..8fef05b --- /dev/null +++ b/src/core/worker.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "worker.h" +#include "utils/time.h" + +namespace giada +{ +Worker::Worker() +: m_running(false) +{ +} + +/* -------------------------------------------------------------------------- */ + +Worker::~Worker() +{ + stop(); +} + +/* -------------------------------------------------------------------------- */ + +void Worker::start(std::function 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 index 0000000..aad217d --- /dev/null +++ b/src/core/worker.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_WORKER_H +#define G_WORKER_H + +#include +#include +#include + +namespace giada +{ +class Worker +{ +public: + Worker(); + ~Worker(); + + void start(std::function f, int sleep); + void stop(); + + private: + std::thread m_thread; + std::atomic 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 index 0000000..337a4a3 --- /dev/null +++ b/src/deps/juce-config.h @@ -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 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 index 0000000..1dc4838 --- /dev/null +++ b/src/ext/resource.h @@ -0,0 +1 @@ +#define IDI_ICON1 101 diff --git a/src/ext/resource.rc b/src/ext/resource.rc new file mode 100644 index 0000000..fb0be40 --- /dev/null +++ b/src/ext/resource.rc @@ -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 index 0000000..7d2441c --- /dev/null +++ b/src/glue/actionEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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(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 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 index 0000000..d624e2a --- /dev/null +++ b/src/glue/actionEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_GLUE_ACTION_EDITOR_H +#define G_GLUE_ACTION_EDITOR_H + +#include "core/types.h" +#include +#include +#include + +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 actions; + + std::optional 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 index 0000000..2e25bc2 --- /dev/null +++ b/src/glue/channel.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +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(c); + else if (c.type == ChannelType::MIDI) + midi = std::make_optional(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 getChannels() +{ + std::vector 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& 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(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 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 index 0000000..c98e01d --- /dev/null +++ b/src/glue/channel.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_GLUE_CHANNEL_H +#define G_GLUE_CHANNEL_H + +#include "core/model/model.h" +#include "core/types.h" +#include +#include +#include +#include + +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 plugins; +#endif + ChannelType type; + Pixel height; + std::string name; + float volume; + float pan; + int key; + bool hasActions; + + std::optional sample; + std::optional 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 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& 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 index 0000000..db2c926 --- /dev/null +++ b/src/glue/config.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 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& 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(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(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 index 0000000..55878c6 --- /dev/null +++ b/src/glue/config.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_GLUE_CONFIG_H +#define G_GLUE_CONFIG_H + +#include "core/kernelAudio.h" +#include "core/types.h" +#include +#include +#include + +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 sampleRates = {}; + + /* Selectable values. */ + + int channelsCount = 0; + int channelsStart = 0; +}; + +struct AudioData +{ + void setOutputDevice(int index); + void setInputDevice(int index); + + std::map apis; + std::vector outputDevices; + std::vector inputDevices; + + /* Selectable values. */ + + int api; + AudioDeviceData outputDevice; + AudioDeviceData inputDevice; + int bufferSize; + int sampleRate; + bool limitOutput; + float recTriggerLevel; + int resampleQuality; +}; + +struct MidiData +{ + std::map apis; + std::map syncModes; + std::vector midiMaps; + std::vector outPorts; + std::vector 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& 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 index 0000000..baa91d1 --- /dev/null +++ b/src/glue/events.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 index 0000000..6897d98 --- /dev/null +++ b/src/glue/events.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..30e2d4f --- /dev/null +++ b/src/glue/io.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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(*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 index 0000000..3a0cf0b --- /dev/null +++ b/src/glue/io.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 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 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 index 0000000..76e7357 --- /dev/null +++ b/src/glue/layout.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..cdc9f5a --- /dev/null +++ b/src/glue/layout.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_GLUE_LAYOUT_H +#define G_GLUE_LAYOUT_H + +#include "core/types.h" +#include +#include + +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); +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 index 0000000..dca6339 --- /dev/null +++ b/src/glue/main.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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 index 0000000..b5bf1b8 --- /dev/null +++ b/src/glue/main.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..86d5798 --- /dev/null +++ b/src/glue/plugin.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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 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 getPluginsInfo() +{ + return g_engine.pluginManager.getPluginsInfo(); +} + +/* -------------------------------------------------------------------------- */ + +void updateWindow(ID pluginId, Thread t) +{ + m::Plugin* p = g_engine.model.findShared(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(g_ui.getSubwindow(*g_ui.mainWindow.get(), WID_FX_LIST)); + if (parent == nullptr) + return; + v::gdPluginWindow* child = static_cast(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 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(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 index 0000000..aaebab7 --- /dev/null +++ b/src/glue/plugin.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 f); + + ID id; + ID channelId; + bool valid; + bool hasEditor; + bool isBypassed; + std::string name; + std::string uniqueId; + int currentProgram; + + std::vector programs; + std::vector paramIndexes; + +private: + m::Plugin& m_plugin; +}; + +struct Plugins +{ + Plugins() = default; + Plugins(const m::Channel&); + + ID channelId; + std::vector 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 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 index 0000000..a25f382 --- /dev/null +++ b/src/glue/recorder.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..efd247e --- /dev/null +++ b/src/glue/recorder.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..942fb9e --- /dev/null +++ b/src/glue/sampleEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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(getSamplePlayer_(channelId).getWave()); +} + +/* -------------------------------------------------------------------------- */ + +/* waveBuffer +A Wave used during cut/copy/paste operations. */ + +std::unique_ptr 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 f) +{ + v::gdSampleEditor* se = static_cast(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(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 index 0000000..780c63b --- /dev/null +++ b/src/glue/sampleEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_GLUE_SAMPLE_EDITOR_H +#define G_GLUE_SAMPLE_EDITOR_H + +#include "core/types.h" +#include "core/waveFx.h" +#include +#include + +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 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 index 0000000..8a7ab66 --- /dev/null +++ b/src/glue/storage.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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(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(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(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(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(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 index 0000000..b6df6dd --- /dev/null +++ b/src/glue/storage.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..ac3ded7 --- /dev/null +++ b/src/gui/dialogs/about.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/conf.h" +#include "core/const.h" +#include "core/graphics.h" +#include +#include +#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 index 0000000..7083d6d --- /dev/null +++ b/src/gui/dialogs/about.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..18c9051 --- /dev/null +++ b/src/gui/dialogs/actionEditor/baseActionEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include +#include + +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 to fix MINMAX macro hell on Windows + zoomAbout([&r = m_ratio]() { return std::max(r / RATIO_STEP, MIN_RATIO); }); +} + +/* -------------------------------------------------------------------------- */ + +void gdBaseActionEditor::zoomOut() +{ + // Explicit type std::max to fix MINMAX macro hell on Windows + zoomAbout([&r = m_ratio]() { return std::min(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 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(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 index 0000000..3ff2f38 --- /dev/null +++ b/src/gui/dialogs/actionEditor/baseActionEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..a270a10 --- /dev/null +++ b/src/gui/dialogs/actionEditor/midiActionEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..4722d65 --- /dev/null +++ b/src/gui/dialogs/actionEditor/midiActionEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..944d4c4 --- /dev/null +++ b/src/gui/dialogs/actionEditor/sampleActionEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..a1f42df --- /dev/null +++ b/src/gui/dialogs/actionEditor/sampleActionEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..c777498 --- /dev/null +++ b/src/gui/dialogs/beatsInput.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..5a68724 --- /dev/null +++ b/src/gui/dialogs/beatsInput.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..b84c655 --- /dev/null +++ b/src/gui/dialogs/bpmInput.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 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 index 0000000..8658e9a --- /dev/null +++ b/src/gui/dialogs/bpmInput.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..1ed0028 --- /dev/null +++ b/src/gui/dialogs/browser/browserBase.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..af92f9c --- /dev/null +++ b/src/gui/dialogs/browser/browserBase.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GD_BROWSER_BASE_H +#define GD_BROWSER_BASE_H + +#include "core/conf.h" +#include "core/types.h" +#include "gui/dialogs/window.h" +#include +#include + +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 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 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 index 0000000..69ea65a --- /dev/null +++ b/src/gui/dialogs/browser/browserDir.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..218d002 --- /dev/null +++ b/src/gui/dialogs/browser/browserDir.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..5b4d90f --- /dev/null +++ b/src/gui/dialogs/browser/browserLoad.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..828276d --- /dev/null +++ b/src/gui/dialogs/browser/browserLoad.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..56f18c0 --- /dev/null +++ b/src/gui/dialogs/browser/browserSave.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..8f07133 --- /dev/null +++ b/src/gui/dialogs/browser/browserSave.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..ed9ac9c --- /dev/null +++ b/src/gui/dialogs/channelNameInput.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..c9286ae --- /dev/null +++ b/src/gui/dialogs/channelNameInput.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..29048a8 --- /dev/null +++ b/src/gui/dialogs/config.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..9c4e5c2 --- /dev/null +++ b/src/gui/dialogs/config.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..51fae3c --- /dev/null +++ b/src/gui/dialogs/keyGrabber.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..6c23efc --- /dev/null +++ b/src/gui/dialogs/keyGrabber.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GD_KEYGRABBER_H +#define GD_KEYGRABBER_H + +#include "core/conf.h" +#include "window.h" +#include +#include + +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 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 index 0000000..c0c6582 --- /dev/null +++ b/src/gui/dialogs/mainWindow.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 index 0000000..f0ae97d --- /dev/null +++ b/src/gui/dialogs/mainWindow.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..c8189c0 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiInputBase.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..6aa6cb3 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiInputBase.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..b41afff --- /dev/null +++ b/src/gui/dialogs/midiIO/midiInputChannel.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "core/conf.h" +#include "core/const.h" +#include "utils/gui.h" +#include "utils/log.h" +#include +#include +#include +#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(m_container->getChild(i++))->update(m_data); +#ifdef WITH_VST + for (c::io::PluginData& plugin : m_data.plugins) + static_cast(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 index 0000000..142ebe3 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiInputChannel.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..2a79e12 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiInputMaster.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..865236f --- /dev/null +++ b/src/gui/dialogs/midiIO/midiInputMaster.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..7635ab9 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiOutputBase.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..28b9e8f --- /dev/null +++ b/src/gui/dialogs/midiIO/midiOutputBase.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..ca31890 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiOutputMidiCh.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..1abc715 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiOutputMidiCh.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..0c0fde3 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiOutputSampleCh.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..668a178 --- /dev/null +++ b/src/gui/dialogs/midiIO/midiOutputSampleCh.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..03b897f --- /dev/null +++ b/src/gui/dialogs/missingAssets.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..bd1ca05 --- /dev/null +++ b/src/gui/dialogs/missingAssets.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..1f06e17 --- /dev/null +++ b/src/gui/dialogs/pluginChooser.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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(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 index 0000000..80d4389 --- /dev/null +++ b/src/gui/dialogs/pluginChooser.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 index 0000000..02fee22 --- /dev/null +++ b/src/gui/dialogs/pluginList.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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(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(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 index 0000000..81d043c --- /dev/null +++ b/src/gui/dialogs/pluginList.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..f4e712a --- /dev/null +++ b/src/gui/dialogs/pluginWindow.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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(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 index 0000000..875d821 --- /dev/null +++ b/src/gui/dialogs/pluginWindow.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..5568c57 --- /dev/null +++ b/src/gui/dialogs/pluginWindowGUI.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#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(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(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 index 0000000..6877c16 --- /dev/null +++ b/src/gui/dialogs/pluginWindowGUI.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifdef WITH_VST + +#ifndef GD_PLUGIN_WINDOW_GUI_H +#define GD_PLUGIN_WINDOW_GUI_H + +#include "deps/juce-config.h" +#include "window.h" +#include +#include +#include + +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 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 index 0000000..a42cb3d --- /dev/null +++ b/src/gui/dialogs/progress.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/dialogs/progress.h" +#include "core/const.h" +#include "deps/geompp/src/rect.hpp" +#include "utils/gui.h" +#include + +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 index 0000000..d9b6759 --- /dev/null +++ b/src/gui/dialogs/progress.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..194748f --- /dev/null +++ b/src/gui/dialogs/sampleEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +#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 index 0000000..fa48b03 --- /dev/null +++ b/src/gui/dialogs/sampleEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..48964fe --- /dev/null +++ b/src/gui/dialogs/warnings.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 index 0000000..22b4a42 --- /dev/null +++ b/src/gui/dialogs/warnings.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..9b0196f --- /dev/null +++ b/src/gui/dialogs/window.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "window.h" +#include "utils/log.h" +#include + +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 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; igetId()); + u::log::print("----\n"); + */ +} + +/* -------------------------------------------------------------------------- */ + +geompp::Rect 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 index 0000000..aac3f19 --- /dev/null +++ b/src/gui/dialogs/window.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GD_WINDOW_H +#define GD_WINDOW_H + +#include "deps/geompp/src/rect.hpp" +#include +#include + +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, 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 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 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 index 0000000..423f75f --- /dev/null +++ b/src/gui/dispatcher.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 index 0000000..81387c6 --- /dev/null +++ b/src/gui/dispatcher.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_V_DISPATCHER_H +#define G_V_DISPATCHER_H + +#include "core/conf.h" +#include "core/types.h" +#include + +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 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 index 0000000..15d192f --- /dev/null +++ b/src/gui/drawing.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "drawing.h" +#include "utils/gui.h" +#include +#include + +namespace giada::v +{ +void drawRectf(geompp::Rect r, Fl_Color c) +{ + fl_rectf(r.x, r.y, r.w, r.h, c); +} + +/* -------------------------------------------------------------------------- */ + +void drawRect(geompp::Rect r, Fl_Color c) +{ + fl_rect(r.x, r.y, r.w, r.h, c); +} + +/* -------------------------------------------------------------------------- */ + +void drawLine(geompp::Line 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 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 index 0000000..2882a0f --- /dev/null +++ b/src/gui/drawing.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_V_DRAWING_H +#define G_V_DRAWING_H + +#include "deps/geompp/src/rect.hpp" +#include +#include + +namespace giada::v +{ +void drawRectf(geompp::Rect, Fl_Color); +void drawRect(geompp::Rect, Fl_Color); +void drawLine(geompp::Line, Fl_Color); +void drawText(const std::string&, geompp::Rect, 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 index 0000000..989f8f0 --- /dev/null +++ b/src/gui/elems/actionEditor/baseAction.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "baseAction.h" +#include +#include + +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 index 0000000..35c3738 --- /dev/null +++ b/src/gui/elems/actionEditor/baseAction.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_BASE_ACTION_H +#define GE_BASE_ACTION_H + +#include "core/types.h" +#include "src/core/actions/actions.h" +#include + +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 index 0000000..2ae40ac --- /dev/null +++ b/src/gui/elems/actionEditor/baseActionEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/dialogs/actionEditor/baseActionEditor.h" +#include "baseAction.h" +#include "baseActionEditor.h" +#include "core/const.h" +#include "core/sequencer.h" +#include "gridTool.h" +#include +#include + +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(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 index 0000000..dd47439 --- /dev/null +++ b/src/gui/elems/actionEditor/baseActionEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_BASE_ACTION_EDITOR_H +#define GE_BASE_ACTION_EDITOR_H + +#include "core/types.h" +#include + +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 index 0000000..e6ad8f9 --- /dev/null +++ b/src/gui/elems/actionEditor/envelopeEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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(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(value, 0, G_MAX_VELOCITY, y() + (h() - geEnvelopePoint::SIDE), y()); +} + +int geEnvelopeEditor::yToValue(Pixel pixel, Pixel offset) const +{ + return u::math::map(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 index 0000000..494492d --- /dev/null +++ b/src/gui/elems/actionEditor/envelopeEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..d39998d --- /dev/null +++ b/src/gui/elems/actionEditor/envelopePoint.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "envelopePoint.h" +#include "core/const.h" +#include + +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 index 0000000..ffc51b0 --- /dev/null +++ b/src/gui/elems/actionEditor/envelopePoint.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..97ce87f --- /dev/null +++ b/src/gui/elems/actionEditor/gridTool.cpp @@ -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 +* . +* +* --------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..ffa9660 --- /dev/null +++ b/src/gui/elems/actionEditor/gridTool.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_GRID_TOOL_H +#define GE_GRID_TOOL_H + +#include "core/conf.h" +#include "core/types.h" +#include + +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 index 0000000..83233e0 --- /dev/null +++ b/src/gui/elems/actionEditor/pianoItem.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "pianoItem.h" +#include "core/const.h" +#include "core/midiEvent.h" +#include "src/core/actions/action.h" +#include "utils/math.h" +#include + +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(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 index 0000000..293aba5 --- /dev/null +++ b/src/gui/elems/actionEditor/pianoItem.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..de357a1 --- /dev/null +++ b/src/gui/elems/actionEditor/pianoRoll.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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(Notes::G): + case static_cast(Notes::E): + case static_cast(Notes::D): + case static_cast(Notes::B): + case static_cast(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(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 index 0000000..4cb3f53 --- /dev/null +++ b/src/gui/elems/actionEditor/pianoRoll.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_PIANO_ROLL_H +#define GE_PIANO_ROLL_H + +#include "baseActionEditor.h" +#include + +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 index 0000000..0c1b2b4 --- /dev/null +++ b/src/gui/elems/actionEditor/sampleAction.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "sampleAction.h" +#include "core/const.h" +#include "src/core/actions/action.h" +#include + +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 index 0000000..13a2736 --- /dev/null +++ b/src/gui/elems/actionEditor/sampleAction.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..16c6eab --- /dev/null +++ b/src/gui/elems/actionEditor/sampleActionEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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(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 index 0000000..23270aa --- /dev/null +++ b/src/gui/elems/actionEditor/sampleActionEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..acd3a22 --- /dev/null +++ b/src/gui/elems/actionEditor/splitScroll.cpp @@ -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 +* . +* +* --------------------------------------------------------------------------- */ + +#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 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 index 0000000..04f5e09 --- /dev/null +++ b/src/gui/elems/actionEditor/splitScroll.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..60e9033 --- /dev/null +++ b/src/gui/elems/actionEditor/velocityEditor.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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(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(v, 0, G_MAX_VELOCITY, y() + (h() - geEnvelopePoint::SIDE), y()); +} + +int geVelocityEditor::yToValue(Pixel px) const +{ + return u::math::map(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 index 0000000..0ceb352 --- /dev/null +++ b/src/gui/elems/actionEditor/velocityEditor.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..cab44a4 --- /dev/null +++ b/src/gui/elems/basics/box.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "box.h" +#include "core/const.h" +#include "utils/gui.h" +#include + +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 index 0000000..6cc6eda --- /dev/null +++ b/src/gui/elems/basics/box.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_BOX_H +#define GE_BOX_H + +#include + +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 index 0000000..5ff93d6 --- /dev/null +++ b/src/gui/elems/basics/boxtypes.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "boxtypes.h" +#include "../../../core/const.h" +#include + +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 index 0000000..2fca0ca --- /dev/null +++ b/src/gui/elems/basics/boxtypes.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_BOXTYPES_H +#define GE_BOXTYPES_H + +#include + +constexpr Fl_Boxtype G_CUSTOM_BORDER_BOX = FL_FREE_BOXTYPE; +constexpr Fl_Boxtype G_CUSTOM_UP_BOX = static_cast(FL_FREE_BOXTYPE + 1); +constexpr Fl_Boxtype G_CUSTOM_DOWN_BOX = static_cast(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 index 0000000..5a91908 --- /dev/null +++ b/src/gui/elems/basics/browser.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..08bcde9 --- /dev/null +++ b/src/gui/elems/basics/browser.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_BROWSER_H +#define GE_BROWSER_H + +#include + +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 index 0000000..f635ad9 --- /dev/null +++ b/src/gui/elems/basics/button.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "button.h" +#include "core/const.h" +#include "utils/gui.h" +#include + +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(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 index 0000000..5bb7ec7 --- /dev/null +++ b/src/gui/elems/basics/button.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_BUTTON_H +#define GE_BUTTON_H + +#include +#include + +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 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 index 0000000..b13b5d9 --- /dev/null +++ b/src/gui/elems/basics/check.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "check.h" +#include "core/const.h" +#include +#include + +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(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 index 0000000..3178a49 --- /dev/null +++ b/src/gui/elems/basics/check.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_CHECK_H +#define GE_CHECK_H + +#include +#include + +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 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 index 0000000..c2a41e9 --- /dev/null +++ b/src/gui/elems/basics/choice.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/elems/basics/choice.h" +#include "core/const.h" +#include "gui/drawing.h" +#include "utils/gui.h" +#include "utils/vector.h" +#include +#include + +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 bounds(x(), y(), w(), h()); + + drawRectf(bounds, G_COLOR_GREY_2); // background + drawRect(bounds, static_cast(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(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(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 index 0000000..f96feac --- /dev/null +++ b/src/gui/elems/basics/choice.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +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 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 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 index 0000000..7f11152 --- /dev/null +++ b/src/gui/elems/basics/dial.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/elems/basics/dial.h" +#include "core/const.h" +#include + +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(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 index 0000000..f33d8db --- /dev/null +++ b/src/gui/elems/basics/dial.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_DIAL_H +#define GE_DIAL_H + +#include +#include + +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 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 index 0000000..070be45 --- /dev/null +++ b/src/gui/elems/basics/flex.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "flex.h" +#include +#include + +namespace giada::v +{ +geFlex::Elem::Elem(Fl_Widget& w, geFlex& parent, Direction d, int size, geompp::Border 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 bounds; + + if (m_dir == Direction::VERTICAL) + bounds = geompp::Rect(m_parent.x(), pos, m_parent.w(), newSize).reduced(m_pad); + else + bounds = geompp::Rect(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 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 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 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 index 0000000..574409e --- /dev/null +++ b/src/gui/elems/basics/flex.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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, 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 pad = {}); + void add(Fl_Widget*, int size = -1, geompp::Border 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 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 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 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 index 0000000..fc50cd2 --- /dev/null +++ b/src/gui/elems/basics/group.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "group.h" +#include +#include + +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 index 0000000..1460911 --- /dev/null +++ b/src/gui/elems/basics/group.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_GROUP_H +#define GE_GROUP_H + +#include +#include +#include + +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 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 index 0000000..0fc1eae --- /dev/null +++ b/src/gui/elems/basics/input.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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(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 index 0000000..59307ff --- /dev/null +++ b/src/gui/elems/basics/input.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_INPUT_H +#define GE_INPUT_H + +#include +#include +#include + +class geInput : public Fl_Input +{ +public: + geInput(int x, int y, int w, int h, const char* l = 0); + + std::function 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 index 0000000..2544c0d --- /dev/null +++ b/src/gui/elems/basics/liquidScroll.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..f56e78b --- /dev/null +++ b/src/gui/elems/basics/liquidScroll.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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, 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 + 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 index 0000000..c4d5446 --- /dev/null +++ b/src/gui/elems/basics/pack.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..10c853a --- /dev/null +++ b/src/gui/elems/basics/pack.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..d1066f4 --- /dev/null +++ b/src/gui/elems/basics/progress.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..7ddea0d --- /dev/null +++ b/src/gui/elems/basics/progress.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_PROGRESS_H +#define GE_PROGRESS_H + +#include + +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 index 0000000..8ec0827 --- /dev/null +++ b/src/gui/elems/basics/resizerBar.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "resizerBar.h" +#include "core/const.h" +#include +#include +#include +#include +#include +#include + +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(parent()); + group->init_sizes(); + group->redraw(); +} + +/* -------------------------------------------------------------------------- */ + +void geResizerBar::move(int diff) +{ + Fl_Widget& wfirst = getFirstWidget(); + std::vector 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 geResizerBar::findWidgets(std::function f, int howmany) const +{ + std::vector out; + Fl_Group* group = static_cast(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 index 0000000..31057fb --- /dev/null +++ b/src/gui/elems/basics/resizerBar.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_RESIZER_BAR_H +#define GE_RESIZER_BAR_H + +#include +#include + +/* 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 onDrag = nullptr; + std::function 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 findWidgets(std::function 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 index 0000000..8f4837c --- /dev/null +++ b/src/gui/elems/basics/scroll.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "scroll.h" +#include "boxtypes.h" +#include "core/const.h" +#include + +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(w->parent()); + Fl_Scrollbar* b = static_cast(w); + + s->scroll_to(s->xposition(), b->value()); + + (static_cast(p))->cb_onScrollV(); +} + +void geScroll::cb_onScrollH(Fl_Widget* w, void* p) +{ + geScroll* s = static_cast(w->parent()); + Fl_Scrollbar* b = static_cast(w); + + s->scroll_to(b->value(), s->yposition()); + + (static_cast(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 index 0000000..d13a8e8 --- /dev/null +++ b/src/gui/elems/basics/scroll.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_SCROLL_H +#define GE_SCROLL_H + +#include +#include + +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 onScrollV{nullptr}; + std::function 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 index 0000000..0b5892a --- /dev/null +++ b/src/gui/elems/basics/scrollPack.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "scrollPack.h" +#include "boxtypes.h" +#include "core/const.h" +#include +#include + +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 index 0000000..14dfe4d --- /dev/null +++ b/src/gui/elems/basics/scrollPack.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_SCROLL_PACK_H +#define GE_SCROLL_PACK_H + +#include "gui/elems/basics/pack.h" +#include "gui/elems/basics/scroll.h" +#include + +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 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 index 0000000..e018452 --- /dev/null +++ b/src/gui/elems/basics/slider.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..5f47744 --- /dev/null +++ b/src/gui/elems/basics/slider.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_SLIDER_H +#define GE_SLIDER_H + +#include + +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 index 0000000..a296082 --- /dev/null +++ b/src/gui/elems/basics/split.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..237db18 --- /dev/null +++ b/src/gui/elems/basics/split.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_SPLIT_H +#define GE_SPLIT_H + +#include "gui/elems/basics/resizerBar.h" +#include + +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 index 0000000..ee6733e --- /dev/null +++ b/src/gui/elems/basics/statusButton.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "statusButton.h" +#include "core/const.h" +#include + +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 index 0000000..1c9a917 --- /dev/null +++ b/src/gui/elems/basics/statusButton.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..b2aeca9 --- /dev/null +++ b/src/gui/elems/basics/tabs.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/elems/basics/tabs.h" +#include "core/const.h" +#include "gui/elems/basics/boxtypes.h" + +namespace giada::v +{ +geTabs::geTabs(geompp::Rect 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 index 0000000..37229f2 --- /dev/null +++ b/src/gui/elems/basics/tabs.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_TABS_H +#define GE_TABS_H + +#include "deps/geompp/src/rect.hpp" +#include + +namespace giada::v +{ +class geTabs : public Fl_Tabs +{ +public: + geTabs(geompp::Rect); + + 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 index 0000000..aaac8de --- /dev/null +++ b/src/gui/elems/config/tabAudio.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +constexpr int LABEL_WIDTH = 110; + +namespace giada::v +{ +geTabAudio::geDeviceMenu::geDeviceMenu(const char* l, const std::vector& 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 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 index 0000000..52d3dee --- /dev/null +++ b/src/gui/elems/config/tabAudio.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +class geCheck; +class geInput; + +namespace giada::v +{ +class geTabAudio : public Fl_Group +{ +public: + struct geDeviceMenu : public geChoice + { + geDeviceMenu(const char* l, const std::vector&); + }; + + 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); + + 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 index 0000000..bcf2494 --- /dev/null +++ b/src/gui/elems/config/tabBehaviors.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +namespace giada::v +{ +geTabBehaviors::geTabBehaviors(geompp::Rect 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 index 0000000..4e488a7 --- /dev/null +++ b/src/gui/elems/config/tabBehaviors.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +namespace giada::v +{ +class geTabBehaviors : public Fl_Group +{ +public: + geTabBehaviors(geompp::Rect, 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 index 0000000..59c07b5 --- /dev/null +++ b/src/gui/elems/config/tabBindings.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..fc6aafe --- /dev/null +++ b/src/gui/elems/config/tabBindings.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_CONFIG_TAB_BINDINGS_H +#define GE_CONFIG_TAB_BINDINGS_H + +#include "core/conf.h" +#include "deps/geompp/src/rect.hpp" +#include + +class geCheck; +class geInput; + +namespace giada::v +{ +class geKeyBinder; +class geTabBindings : public Fl_Group +{ +public: + geTabBindings(geompp::Rect, 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 index 0000000..c34e628 --- /dev/null +++ b/src/gui/elems/config/tabMidi.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +constexpr int LABEL_WIDTH = 120; + +namespace giada::v +{ +geTabMidi::geMenu::geMenu(const char* l, const std::vector& 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 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 index 0000000..3add0b5 --- /dev/null +++ b/src/gui/elems/config/tabMidi.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +class geCheck; + +namespace giada::v +{ +class geTabMidi : public Fl_Group +{ +public: + struct geMenu : public geChoice + { + geMenu(const char* l, const std::vector&, const std::string& msgIfNotFound); + }; + + geTabMidi(geompp::Rect); + + 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 index 0000000..8cf108e --- /dev/null +++ b/src/gui/elems/config/tabMisc.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 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 index 0000000..f2d3c5a --- /dev/null +++ b/src/gui/elems/config/tabMisc.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_TAB_MISC_H +#define GE_TAB_MISC_H + +#include "deps/geompp/src/rect.hpp" +#include "glue/config.h" +#include + +namespace giada::v +{ +class geChoice; +class geTabMisc : public Fl_Group +{ +public: + geTabMisc(geompp::Rect); + + 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 index 0000000..43f5507 --- /dev/null +++ b/src/gui/elems/config/tabPlugins.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +namespace giada::v +{ +geTabPlugins::geTabPlugins(geompp::Rect 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(top_window())); + }; + + m_scanButton->onClick = [this]() { + std::function 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 index 0000000..2eea680 --- /dev/null +++ b/src/gui/elems/config/tabPlugins.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_TAB_PLUGINS_H +#define GE_TAB_PLUGINS_H + +#ifdef WITH_VST + +#include "deps/geompp/src/rect.hpp" +#include "glue/config.h" +#include + +class geInput; +class geBox; + +namespace giada::v +{ +class geBox; +class geButton; +class geTabPlugins : public Fl_Group +{ +public: + geTabPlugins(geompp::Rect); + + 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 index 0000000..db021e2 --- /dev/null +++ b/src/gui/elems/fileBrowser.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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(parent())->fireCallback(); + ret = 1; + break; + case FL_PUSH: // mouse + if (Fl::event_clicks() > 0) // double click + static_cast(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 index 0000000..624a86a --- /dev/null +++ b/src/gui/elems/fileBrowser.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_FILE_BROWSER_H +#define GE_FILE_BROWSER_H + +#include +#include + +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 index 0000000..93d2c3f --- /dev/null +++ b/src/gui/elems/keyBinder.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..d91eb75 --- /dev/null +++ b/src/gui/elems/keyBinder.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_KEY_BINDER_H +#define GE_KEY_BINDER_H + +#include "gui/elems/basics/flex.h" +#include + +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 index 0000000..0b62a1d --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/channel.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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(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 index 0000000..c457b46 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/channel.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_CHANNEL_H +#define GE_CHANNEL_H + +#include "core/types.h" +#include "glue/channel.h" +#include + +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 index 0000000..cb965d7 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/channelButton.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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(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 index 0000000..400ed3c --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/channelButton.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..a701ee1 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/channelMode.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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(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(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 index 0000000..003d98f --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/channelMode.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_CHANNEL_MODE_H +#define GE_CHANNEL_MODE_H + +#include + +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 index 0000000..53cb7db --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/channelStatus.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/elems/mainWindow/keyboard/channelStatus.h" +#include "core/const.h" +#include "glue/channel.h" +#include "utils/math.h" +#include + +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 index 0000000..595557b --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/channelStatus.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_CHANNEL_STATUS_H +#define GE_CHANNEL_STATUS_H + +#include + +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 index 0000000..237ac61 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/column.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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(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(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 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 index 0000000..808650c --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/column.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_COLUMN_H +#define GE_COLUMN_H + +#include "core/types.h" +#include "glue/channel.h" +#include +#include +#include + +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 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 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 index 0000000..4c72ca3 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/keyboard.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 f) const +{ + for (geColumn* column : m_columns) + column->forEachChannel(f); +} + +void geKeyboard::forEachColumn(std::function 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(const_cast(this)->getChannel(channelId)); +} + +/* -------------------------------------------------------------------------- */ + +std::vector geKeyboard::getDroppedFilePaths() const +{ + std::vector 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 index 0000000..c5593f8 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/keyboard.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_KEYBOARD_H +#define GE_KEYBOARD_H + +#include "core/const.h" +#include "core/idManager.h" +#include "gui/elems/basics/scroll.h" +#include +#include + +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 f) const; + void forEachColumn(std::function f) const; + + /* layout + The column layout. Each element is a column with a specific width. */ + + std::vector 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 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 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 index 0000000..4ced13c --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/midiActivity.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/elems/mainWindow/keyboard/midiActivity.h" +#include "core/const.h" +#include "gui/elems/basics/flex.h" +#include + +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 index 0000000..f402a2e --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/midiActivity.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_MIDI_ACTIVITY_H +#define GE_MIDI_ACTIVITY_H + +#include +#include + +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 index 0000000..3273c85 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/midiChannel.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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(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 index 0000000..9fc4db3 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/midiChannel.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..4c5cca9 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/midiChannelButton.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..3337b5a --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/midiChannelButton.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..f2759e4 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/sampleChannel.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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(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 index 0000000..f1f6d36 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/sampleChannel.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..e319ac3 --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/sampleChannelButton.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..b09737a --- /dev/null +++ b/src/gui/elems/mainWindow/keyboard/sampleChannelButton.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..8b3fca8 --- /dev/null +++ b/src/gui/elems/mainWindow/mainIO.cpp @@ -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 +* . +* +* --------------------------------------------------------------------------- */ + +#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 index 0000000..9b83ccc --- /dev/null +++ b/src/gui/elems/mainWindow/mainIO.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..0823313 --- /dev/null +++ b/src/gui/elems/mainWindow/mainMenu.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..4ca3823 --- /dev/null +++ b/src/gui/elems/mainWindow/mainMenu.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..737b475 --- /dev/null +++ b/src/gui/elems/mainWindow/mainTimer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..c1ee8d5 --- /dev/null +++ b/src/gui/elems/mainWindow/mainTimer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..c0d680a --- /dev/null +++ b/src/gui/elems/mainWindow/mainTransport.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..f9e97a4 --- /dev/null +++ b/src/gui/elems/mainWindow/mainTransport.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..f0e38a5 --- /dev/null +++ b/src/gui/elems/mainWindow/sequencer.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "sequencer.h" +#include "core/const.h" +#include "gui/drawing.h" +#include "utils/math.h" +#include + +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 index 0000000..1d6dab2 --- /dev/null +++ b/src/gui/elems/mainWindow/sequencer.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_SEQUENCER_H +#define GE_SEQUENCER_H + +#include "core/types.h" +#include "deps/geompp/src/rect.hpp" +#include "glue/main.h" +#include + +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 m_background; + geompp::Rect 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 index 0000000..e730c23 --- /dev/null +++ b/src/gui/elems/midiIO/midiLearner.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..393c005 --- /dev/null +++ b/src/gui/elems/midiIO/midiLearner.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_MIDI_LEARNER_H +#define GE_MIDI_LEARNER_H + +#include "gui/elems/basics/flex.h" +#include +#include + +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 onStartLearn; + std::function onStopLearn; + std::function 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 index 0000000..c79b1ef --- /dev/null +++ b/src/gui/elems/midiIO/midiLearnerPack.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "midiLearnerPack.h" +#include "core/const.h" +#include "glue/io.h" +#include "gui/elems/basics/box.h" +#include + +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 s, std::function 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 index 0000000..1622150 --- /dev/null +++ b/src/gui/elems/midiIO/midiLearnerPack.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_LEARNER_PACK_H +#define GE_LEARNER_PACK_H + +#include "gui/elems/basics/pack.h" +#include "gui/elems/midiIO/midiLearner.h" +#include +#include + +namespace giada +{ +namespace v +{ +class geMidiLearnerPack : public gePack +{ +public: + geMidiLearnerPack(int x, int y, std::string title = ""); + + void setCallbacks(std::function, std::function); + void addMidiLearner(std::string label, int param, bool visible = true); + void setEnabled(bool v); + + std::vector learners; + + private: + std::function m_onStartLearn; + std::function 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 index 0000000..2a4d3dd --- /dev/null +++ b/src/gui/elems/plugin/pluginBrowser.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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(fl_width(pi.name.c_str())); + w1 = static_cast(fl_width(pi.manufacturerName.c_str())); + w3 = static_cast(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(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 index 0000000..0b37df0 --- /dev/null +++ b/src/gui/elems/plugin/pluginBrowser.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifdef WITH_VST + +#ifndef GE_PLUGIN_BROWSER_H +#define GE_PLUGIN_BROWSER_H + +#include + +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 index 0000000..34eecda --- /dev/null +++ b/src/gui/elems/plugin/pluginElement.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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(window()); + + c::plugin::swapPlugins(m_plugin.getPluginRef(), parent->getPrevElement(*this).getPluginRef(), m_plugin.channelId); +} + +/* -------------------------------------------------------------------------- */ + +void gePluginElement::cb_shiftDown() +{ + const gdPluginList* parent = static_cast(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(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(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 index 0000000..9277e4d --- /dev/null +++ b/src/gui/elems/plugin/pluginElement.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..4d2c97a --- /dev/null +++ b/src/gui/elems/plugin/pluginParameter.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..1db9f93 --- /dev/null +++ b/src/gui/elems/plugin/pluginParameter.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifdef WITH_VST + +#ifndef GE_PLUGIN_PARAMETER_H +#define GE_PLUGIN_PARAMETER_H + +#include "core/types.h" +#include "glue/plugin.h" +#include + +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 index 0000000..5c8dea0 --- /dev/null +++ b/src/gui/elems/sampleEditor/boostTool.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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(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(window())->ch; + + c::channel::setBoost(ch->id, dial->value());*/ +} + +/* -------------------------------------------------------------------------- */ + +void geBoostTool::cb_setBoostNum() +{ + /*const m::SampleChannel* ch = static_cast(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 index 0000000..a01f9c3 --- /dev/null +++ b/src/gui/elems/sampleEditor/boostTool.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_BOOST_TOOL_H +#define GE_BOOST_TOOL_H + +#include + +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 index 0000000..bcf685b --- /dev/null +++ b/src/gui/elems/sampleEditor/panTool.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..5d97aa0 --- /dev/null +++ b/src/gui/elems/sampleEditor/panTool.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..69cad2b --- /dev/null +++ b/src/gui/elems/sampleEditor/pitchTool.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 + +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 index 0000000..f206627 --- /dev/null +++ b/src/gui/elems/sampleEditor/pitchTool.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..8f8e85d --- /dev/null +++ b/src/gui/elems/sampleEditor/rangeTool.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 index 0000000..ed37e5e --- /dev/null +++ b/src/gui/elems/sampleEditor/rangeTool.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..f5c56c3 --- /dev/null +++ b/src/gui/elems/sampleEditor/shiftTool.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 index 0000000..09ec99e --- /dev/null +++ b/src/gui/elems/sampleEditor/shiftTool.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..5fd7d25 --- /dev/null +++ b/src/gui/elems/sampleEditor/volumeTool.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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 index 0000000..2b75340 --- /dev/null +++ b/src/gui/elems/sampleEditor/volumeTool.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..e271d3c --- /dev/null +++ b/src/gui/elems/sampleEditor/waveTools.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include + +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(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 index 0000000..76481da --- /dev/null +++ b/src/gui/elems/sampleEditor/waveTools.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_WAVE_TOOLS_H +#define GE_WAVE_TOOLS_H + +#include + +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 index 0000000..761434e --- /dev/null +++ b/src/gui/elems/sampleEditor/waveform.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include +#include +#include + +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(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 index 0000000..0d68338 --- /dev/null +++ b/src/gui/elems/sampleEditor/waveform.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_WAVEFORM_H +#define GE_WAVEFORM_H + +#include "core/const.h" +#include "core/types.h" +#include +#include + +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 sup; // upper part of the waveform + std::vector 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 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 index 0000000..816ce9f --- /dev/null +++ b/src/gui/elems/soundMeter.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "soundMeter.h" +#include "core/const.h" +#include "core/kernelAudio.h" +#include "core/types.h" +#include "gui/drawing.h" +#include "utils/math.h" +#include +#include +#include + +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 index 0000000..0925e3f --- /dev/null +++ b/src/gui/elems/soundMeter.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef GE_SOUND_METER_H +#define GE_SOUND_METER_H + +#include "core/types.h" +#include + +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 index 0000000..97dee84 --- /dev/null +++ b/src/gui/types.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..1b1e732 --- /dev/null +++ b/src/gui/ui.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#if defined(G_OS_LINUX) || defined(G_OS_FREEBSD) +#include // 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(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 index 0000000..116eaf0 --- /dev/null +++ b/src/gui/ui.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 +#include + +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 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 index 0000000..cbb2f5a --- /dev/null +++ b/src/gui/updater.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "gui/updater.h" +#include "core/const.h" +#include "core/model/model.h" +#include "gui/ui.h" +#include + +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(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 index 0000000..5b592d8 --- /dev/null +++ b/src/gui/updater.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..065002f --- /dev/null +++ b/src/main.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..42a5ac7 --- /dev/null +++ b/src/utils/cocoa.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..10792b1 --- /dev/null +++ b/src/utils/cocoa.mm @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + + + #import + #import + #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 index 0000000..6f19795 --- /dev/null +++ b/src/utils/fs.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include +#if defined(_WIN32) // getcwd (unix) or __getcwd (win) +#include +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include // stat (fs::dirExists) +#ifdef __APPLE__ +#include // basename unix +#include // 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 index 0000000..b1f608e --- /dev/null +++ b/src/utils/fs.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_UTILS_FS_H +#define G_UTILS_FS_H + +#include + +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 index 0000000..da5b7a5 --- /dev/null +++ b/src/utils/gui.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include +#include +#include +#include +#if defined(_WIN32) +#include "../ext/resource.h" +#elif defined(__linux__) || defined(__FreeBSD__) +#include +#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 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 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 index 0000000..dec6721 --- /dev/null +++ b/src/utils/gui.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_UTILS_GUI_H +#define G_UTILS_GUI_H + +#include "core/types.h" +#include "deps/geompp/src/rect.hpp" +#include +#include + +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 getStringRect(const std::string& s); + +/* getCenterWinBounds +Returns the bounding box to be used for a centered window. */ + +geompp::Rect 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 index 0000000..42a1d0f --- /dev/null +++ b/src/utils/log.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "log.h" +#include +#include + +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 index 0000000..2413724 --- /dev/null +++ b/src/utils/log.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_UTILS_LOG_H +#define G_UTILS_LOG_H + +#include "core/const.h" +#include "utils/fs.h" +#include +#include +#include +#include + +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::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 +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))...); +#ifdef _WIN32 + fflush(f); +#endif + } + else + std::printf(format, string_to_c_str(std::forward(args))...); +} +} // namespace giada::u::log + +#endif diff --git a/src/utils/math.cpp b/src/utils/math.cpp new file mode 100644 index 0000000..9f38a2f --- /dev/null +++ b/src/utils/math.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "math.h" +#include + +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 index 0000000..7c31338 --- /dev/null +++ b/src/utils/math.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_UTILS_MATH_H +#define G_UTILS_MATH_H + +#include +#include + +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 +TO map(TI x, TI a, TI b, TO w, TO z) +{ + static_assert(std::is_arithmetic_v); + static_assert(std::is_arithmetic_v); + + 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 +TO map(TI x, TI b, TO z) +{ + return map(x, static_cast(0), b, static_cast(0), z); +} +} // namespace giada::u::math + +#endif diff --git a/src/utils/string.cpp b/src/utils/string.cpp new file mode 100644 index 0000000..f50c858 --- /dev/null +++ b/src/utils/string.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "string.h" +#include "core/const.h" +#include +#include +#include +#include +#include + +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 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 split(std::string in, std::string sep) +{ + std::vector 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 index 0000000..a4d5a8d --- /dev/null +++ b/src/utils/string.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_UTILS_STRING_H +#define G_UTILS_STRING_H + +#include +#include +#include + +namespace giada +{ +namespace u +{ +namespace string +{ +template +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 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 index 0000000..b95c467 --- /dev/null +++ b/src/utils/time.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "time.h" +#include +#include + +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 index 0000000..f79c345 --- /dev/null +++ b/src/utils/time.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#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 index 0000000..dfbcace --- /dev/null +++ b/src/utils/vector.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_UTILS_VECTOR_H +#define G_UTILS_VECTOR_H + +#include +#include +#include +#include + +namespace giada::u::vector +{ +template +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 +auto findIf(const T& v, F&& func) +{ + return std::find_if(std::cbegin(v), std::cend(v), func); +} + +/* -------------------------------------------------------------------------- */ + +template +bool has(const T& v, F&& func) +{ + return findIf(v, func) != std::cend(v); +} + +/* -------------------------------------------------------------------------- */ + +template +void removeIf(T& v, F&& func) +{ + v.erase(std::remove_if(v.begin(), v.end(), func), v.end()); +} + +template +void remove(T& v, const V& o) +{ + v.erase(std::remove(v.begin(), v.end(), o), v.end()); +} + +/* -------------------------------------------------------------------------- */ + +template +std::vector cast(const I& i) +{ + return {i.begin(), i.end()}; +} + +/* -------------------------------------------------------------------------- */ + +template +auto atOr(const Vector& v, int index, Default d) +{ + return index >= 0 && static_cast(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 index 0000000..a4c0470 --- /dev/null +++ b/src/utils/ver.cpp @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#include "ver.h" +#include "core/const.h" +#include "deps/rtaudio/RtAudio.h" +#include +#include + +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 index 0000000..7f96f85 --- /dev/null +++ b/src/utils/ver.h @@ -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 + * . + * + * -------------------------------------------------------------------------- */ + +#ifndef G_UTILS_VER_H +#define G_UTILS_VER_H + +#include + +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 index 0000000..e0f884c --- /dev/null +++ b/tests/actionRecorder.cpp @@ -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 + +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 index 0000000..b76f5ca --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,3 @@ +#define CATCH_CONFIG_MAIN +#define CATCH_CONFIG_FAST_COMPILE +#include \ No newline at end of file diff --git a/tests/midiLighter.cpp b/tests/midiLighter.cpp new file mode 100644 index 0000000..fc729be --- /dev/null +++ b/tests/midiLighter.cpp @@ -0,0 +1,84 @@ +#include "../src/core/channels/midiLighter.h" +#include "mocks/kernelMidiMock.h" +#include +#include + +TEST_CASE("MidiMapper") +{ + using namespace giada; + + m::KernelMidiMock kernelMidi; + m::MidiMapper 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 index 0000000..8929b1a --- /dev/null +++ b/tests/mocks/kernelMidiMock.h @@ -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 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 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 index 0000000..1c6a267 --- /dev/null +++ b/tests/samplePlayer.cpp @@ -0,0 +1,91 @@ +#include "../src/core/channels/samplePlayer.h" +#include + +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(i + 1); + f[1] = static_cast(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 index 0000000..4ec2a26 --- /dev/null +++ b/tests/utils.cpp @@ -0,0 +1,64 @@ +#include "../src/utils/fs.h" +#include "../src/utils/math.h" +#include "../src/utils/string.h" +#include + +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 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 index 0000000..0a622f8 --- /dev/null +++ b/tests/wave.cpp @@ -0,0 +1,47 @@ +#include "../src/core/wave.h" +#include +#include + +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 index 0000000..f045846 --- /dev/null +++ b/tests/waveFx.cpp @@ -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 +#include + +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 index 0000000..bd32731 --- /dev/null +++ b/tests/waveManager.cpp @@ -0,0 +1,60 @@ +#include "../src/core/waveManager.h" +#include "../src/core/const.h" +#include "../src/core/wave.h" +#include +#include +#include + +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 = 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 index 0000000..ef7b587 --- /dev/null +++ b/tests/waveReader.cpp @@ -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 +#include + +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(i + 1); + f[1] = static_cast(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); + } + } +}