diff --git a/.travis/linux..install.sh b/.travis/linux..install.sh index 2f1262d071b..e440d794662 100755 --- a/.travis/linux..install.sh +++ b/.travis/linux..install.sh @@ -3,7 +3,7 @@ set -e PACKAGES="cmake libsndfile-dev fftw3-dev libvorbis-dev libogg-dev libmp3lame-dev - libasound2-dev libjack-jackd2-dev libsdl-dev libsamplerate0-dev libstk0-dev stk + libasound2-dev libjack-jackd2-dev libsdl2-dev libsamplerate0-dev libstk0-dev stk libfluidsynth-dev portaudio19-dev g++-multilib libfltk1.3-dev libgig-dev libsoundio-dev qt59base qt59translations qt59tools" diff --git a/.travis/linux.win.install_raw.sh b/.travis/linux.win.install_raw.sh new file mode 100755 index 00000000000..67568ac5057 --- /dev/null +++ b/.travis/linux.win.install_raw.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +while read -r PACKAGE_URL_AND_OPTS; do + pushd "$PWD" + IFS=',' read -ra PACKAGE_URL_AND_OPTS <<< "$PACKAGE_URL_AND_OPTS" + + PACKAGE_URL="${PACKAGE_URL_AND_OPTS[0]}" + OPTS="${PACKAGE_URL_AND_OPTS[1]}" + + echo "Downloading $PACKAGE_URL ..." + + mkdir PACKAGE_URL_TEMP + cd PACKAGE_URL_TEMP || exit 1 + + curl "$PACKAGE_URL" | tar xfz - + dir_name=$(ls) + cd "$dir_name" || exit 1 + + echo "Installing package $dir_name (make ${OPTS}) ..." + + # shellcheck disable=SC2086 + sudo make $OPTS + + popd + rm -rf PACKAGE_URL_TEMP +done <<< "$MANUAL_PACKAGES_URLS" + diff --git a/.travis/linux.win32.install.sh b/.travis/linux.win32.install.sh index 6e73e7abea5..fd83e6968c3 100755 --- a/.travis/linux.win32.install.sh +++ b/.travis/linux.win32.install.sh @@ -9,7 +9,9 @@ MINGW_PACKAGES="mingw32-x-sdl mingw32-x-libvorbis mingw32-x-fluidsynth mingw32-x mingw32-x-libgig mingw32-x-libsoundio mingw32-x-lame mingw32-x-qt5base" # swh build dependencies -SWH_PACKAGES="perl libxml2-utils libxml-perl liblist-moreutils-perl" +SWH_PACKAGES="perl libxml2-utils libxml-perl liblist-moreutils-perl libjack-dev" + +export MANUAL_PACKAGES_URLS="https://www.libsdl.org/release/SDL2-devel-2.0.7-mingw.tar.gz,install-package arch=i686-w64-mingw32 prefix=/opt/mingw32" export MINGW_PACKAGES @@ -20,6 +22,8 @@ PACKAGES="nsis cloog-isl libmpc3 qt4-linguist-tools mingw32 $MINGW_PACKAGES $SWH # shellcheck disable=SC2086 sudo apt-get install -y $PACKAGES +"$TRAVIS_BUILD_DIR/.travis/linux.win.install_raw.sh" + # ccache 3.2 is needed because mingw32-x-gcc is version 4.9, which causes cmake # to use @file command line passing, which in turn ccache 3.1.9 doesn't support pushd /tmp diff --git a/.travis/linux.win64.install.sh b/.travis/linux.win64.install.sh index 99ef7187f02..dc84601c488 100755 --- a/.travis/linux.win64.install.sh +++ b/.travis/linux.win64.install.sh @@ -10,6 +10,8 @@ MINGW_PACKAGES="mingw64-x-sdl mingw64-x-libvorbis mingw64-x-fluidsynth mingw64-x mingw64-x-fftw mingw64-x-flac mingw64-x-fltk mingw64-x-libsamplerate mingw64-x-pkgconfig mingw64-x-binutils mingw64-x-gcc mingw64-x-runtime mingw64-x-libgig mingw64-x-libsoundio mingw64-x-lame mingw64-x-qt5base" + +export MANUAL_PACKAGES_URLS="https://www.libsdl.org/release/SDL2-devel-2.0.7-mingw.tar.gz,install-package arch=x86_64-w64-mingw32 prefix=/opt/mingw64" export MINGW_PACKAGES @@ -17,3 +19,5 @@ export MINGW_PACKAGES # shellcheck disable=SC2086 sudo apt-get install -y $MINGW_PACKAGES + +"$TRAVIS_BUILD_DIR/.travis/linux.win.install_raw.sh" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f9d66defcb..a4840855e47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,7 @@ ENDIF(LMMS_BUILD_APPLE) IF(LMMS_BUILD_WIN32) SET(WANT_ALSA OFF) - SET(WANT_JACK OFF) + SET(WANT_JACK ON) SET(WANT_PULSEAUDIO OFF) SET(WANT_SNDIO OFF) SET(WANT_SOUNDIO OFF) @@ -140,7 +140,7 @@ CHECK_INCLUDE_FILES(locale.h LMMS_HAVE_LOCALE_H) LIST(APPEND CMAKE_PREFIX_PATH "${CMAKE_INSTALL_PREFIX}") -FIND_PACKAGE(Qt5 COMPONENTS Core Gui Widgets Xml REQUIRED) +FIND_PACKAGE(Qt5 COMPONENTS Core Gui Widgets Xml Concurrent REQUIRED) FIND_PACKAGE(Qt5 COMPONENTS LinguistTools QUIET) INCLUDE_DIRECTORIES( @@ -382,21 +382,49 @@ ENDIF(NOT LMMS_HAVE_ALSA) IF(WANT_JACK) PKG_CHECK_MODULES(JACK jack>=0.77) IF(JACK_FOUND) - IF(WANT_WEAKJACK) - SET(LMMS_HAVE_WEAKJACK TRUE) - SET(WEAKJACK_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/src/3rdparty/weakjack/weakjack) + SET(STATUS_JACK "OK") + SET(LMMS_HAVE_JACK TRUE) + ELSE() + SET(JACK_INCLUDE_DIRS "") + SET(STATUS_JACK "not found, please install libjack0.100.0-dev (or similar) " + "if you require JACK support or enable weak jack") + ENDIF() + IF(WANT_WEAKJACK) + SET(WEAKJACK_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/src/3rdparty/weakjack/weakjack) + + # We only need Jack's header files, so finding the host's headers in the case + # of cross-compiling is fine. + find_path(JACK_INCLUDE_DIR NAMES jack/jack.h PATHS /usr/include ${JACK_INCLUDE_DIRS} CMAKE_FIND_ROOT_PATH_BOTH) + IF(JACK_INCLUDE_DIR) SET(STATUS_JACK "OK (weak linking enabled)") # use dlsym instead SET(JACK_LIBRARIES ${CMAKE_DL_LIBS}) + SET(LMMS_HAVE_JACK TRUE) + SET(LMMS_HAVE_WEAKJACK TRUE) + + IF(NOT CMAKE_CROSSCOMPILING) + SET(JACK_INCLUDE_DIRS "${JACK_INCLUDE_DIR}") + ELSEIF(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + # We're cross-compiling but possibly using the host's jack + # library, so we need to use "-idirafter" to avoid conflicts + # between the host's and the target's header files. -idirafter + # will make sure that ${JACK_INCLUDE_DIR} is only considered for + # #include instructions after all other search directories have + # been exhausted. With normal "-I", we end up including the + # host's standard library header files, leading to confusing + # compilation errors due to conflicting declarations. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -idirafter ${JACK_INCLUDE_DIR}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -idirafter ${JACK_INCLUDE_DIR}") + ELSE() + SET(STATUS_JACK "Host's jack headers were found, but you're using an unknown cross-compiler. Try to use MinGW or supply a jack library for the target architecture at ${CMAKE_FIND_ROOT_PATHS}") + SET(LMMS_HAVE_JACK FALSE) + SET(LMMS_HAVE_WEAKJACK FALSE) + ENDIF() ELSE() - SET(STATUS_JACK "OK") + SET(STATUS_JACK "no jack headers found, please install libjack0.100.0-dev (or similar) ") + SET(LMMS_HAVE_JACK) ENDIF() - SET(LMMS_HAVE_JACK TRUE) - ELSE(JACK_FOUND) - SET(JACK_INCLUDE_DIRS "") - SET(STATUS_JACK "not found, please install libjack0.100.0-dev (or similar) " - "if you require JACK support") - ENDIF(JACK_FOUND) + ENDIF() ENDIF(WANT_JACK) # check for FFTW3F-library diff --git a/cmake/nsis/CMakeLists.txt b/cmake/nsis/CMakeLists.txt index ac628d549a4..0b3f250f774 100644 --- a/cmake/nsis/CMakeLists.txt +++ b/cmake/nsis/CMakeLists.txt @@ -21,8 +21,7 @@ SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " \\\${registerExtension} \\\"$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe\\\" \\\".mmpz\\\" \\\"${PROJECT_NAME_UCASE} Project (compressed)\\\" \\\${IfNot} \\\${AtMostWin7} WriteRegDWORD HKLM \\\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\SideBySide\\\" \\\"PreferExternalManifest\\\" \\\"1\\\" - \\\${EndIf} - " PARENT_SCOPE) + \\\${EndIf}") SET(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " \\\${unregisterExtension} \\\".mmp\\\" \\\"${PROJECT_NAME_UCASE} Project\\\" \\\${unregisterExtension} \\\".mmpz\\\" \\\"${PROJECT_NAME_UCASE} Project (compressed)\\\" @@ -74,6 +73,27 @@ IF(LMMS_HAVE_STK) INSTALL(FILES ${RAWWAVES} DESTINATION "${DATA_DIR}/stk/rawwaves") ENDIF() +# Bundle jack +IF(LMMS_BUILD_WIN32 AND WANT_JACK) + SET(JACK_INSTALLER_TAG "1.9.11") + SET(JACK_INSTALLER_FILENAME "Jack_v1.9.11_32_setup.exe") + IF(LMMS_BUILD_WIN64) + SET(JACK_INSTALLER_FILENAME "Jack_v1.9.11_64_setup.exe") + ENDIF() + + INSTALL(CODE + "FILE(DOWNLOAD + \"https://github.com/jackaudio/jackaudio.github.com/releases/download/${JACK_INSTALLER_TAG}/${JACK_INSTALLER_FILENAME}\" + \"${CMAKE_BINARY_DIR}/${JACK_INSTALLER_FILENAME}\" + )" + ) + INSTALL(FILES "${CMAKE_BINARY_DIR}/${JACK_INSTALLER_FILENAME}" DESTINATION .) + SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} + ExecWait '\\\"$INSTDIR\\\\${JACK_INSTALLER_FILENAME}\\\" /install'") +ENDIF() + +SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE) + INSTALL(FILES "lmms.exe.manifest" DESTINATION .) INSTALL(FILES "lmms.VisualElementsManifest.xml" DESTINATION .) INSTALL(DIRECTORY "assets" DESTINATION .) diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 5d889295cae..08f778c29ec 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -646,6 +646,7 @@ TrackContentObjectView { qproperty-textColor: #fff; qproperty-textBackgroundColor: rgba(0, 0, 0, 75); qproperty-textShadowColor: rgba(0,0,0,200); + qproperty-recordingBackgroundColor: rgba(139,25,25,0.38); qproperty-gradient: false; /* boolean property, set true to have a gradient */ font-size: 11px; diff --git a/include/AudioJack.h b/include/AudioJack.h index c3207c82984..81477190575 100644 --- a/include/AudioJack.h +++ b/include/AudioJack.h @@ -38,6 +38,8 @@ #include #include +#include + #include "AudioDevice.h" #include "AudioDeviceSetupWidget.h" @@ -102,6 +104,9 @@ private slots: void * _udata ); static void shutdownCallback( void * _udata ); + void connectJackPort (const char *outputPort, + const char *inputPort, + const char *portName); jack_client_t * m_client; @@ -110,7 +115,10 @@ private slots: MidiJack *m_midiClient; QVector m_outputPorts; + QVector m_inputPorts; jack_default_audio_sample_t * * m_tempOutBufs; + jack_default_audio_sample_t * * m_tempInBufs; + std::vector m_inBuffer; surroundSampleFrame * m_outBuf; f_cnt_t m_framesDoneInCurBuf; diff --git a/include/AudioPulseAudio.h b/include/AudioPulseAudio.h index 49674669134..4b15d1bb282 100644 --- a/include/AudioPulseAudio.h +++ b/include/AudioPulseAudio.h @@ -72,12 +72,16 @@ class AudioPulseAudio : public QThread, public AudioDevice void streamWriteCallback( pa_stream * s, size_t length ); + void streamReadCallback(pa_stream *s, size_t length); void signalConnected( bool connected ); pa_stream * m_s; pa_sample_spec m_sampleSpec; + pa_stream * m_recordStream; + pa_sample_spec m_recordSampleSpec; + private: virtual void startProcessing(); @@ -87,8 +91,6 @@ class AudioPulseAudio : public QThread, public AudioDevice volatile bool m_quit; - bool m_convertEndian; - bool m_connected; QSemaphore m_connectedSemaphore; diff --git a/include/AudioSampleRecorder.h b/include/AudioSampleRecorder.h deleted file mode 100644 index 69ac1949047..00000000000 --- a/include/AudioSampleRecorder.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * AudioSampleRecorder.h - device-class that implements recording - * audio-buffers into RAM - * - * Copyright (c) 2004-2014 Tobias Doerffel - * - * This file is part of LMMS - https://lmms.io - * - * 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 2 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 (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - -#ifndef AUDIO_SAMPLE_RECORDER_H -#define AUDIO_SAMPLE_RECORDER_H - -#include -#include - -#include "AudioDevice.h" - -class SampleBuffer; - - -class AudioSampleRecorder : public AudioDevice -{ -public: - AudioSampleRecorder( const ch_cnt_t _channels, bool & _success_ful, - Mixer* mixer ); - virtual ~AudioSampleRecorder(); - - f_cnt_t framesRecorded() const; - void createSampleBuffer( SampleBuffer** sampleBuffer ); - - -private: - virtual void writeBuffer( const surroundSampleFrame * _ab, - const fpp_t _frames, - const float _master_gain ); - - typedef QList > BufferList; - BufferList m_buffers; - -} ; - - -#endif diff --git a/include/AutomationTrack.h b/include/AutomationTrack.h index 195c21e9d4b..5e3ff596354 100644 --- a/include/AutomationTrack.h +++ b/include/AutomationTrack.h @@ -46,7 +46,7 @@ class AutomationTrack : public Track } virtual TrackView * createView( TrackContainerView* ); - virtual TrackContentObject * createTCO( const MidiTime & _pos ); + virtual TrackContentObject * unsafeCreateTCO( const MidiTime & _pos ); virtual void saveTrackSpecificSettings( QDomDocument & _doc, QDomElement & _parent ); @@ -61,6 +61,7 @@ class AutomationTrack : public Track class AutomationTrackView : public TrackView { + Q_OBJECT public: AutomationTrackView( AutomationTrack* at, TrackContainerView* tcv ); virtual ~AutomationTrackView() = default; @@ -68,6 +69,11 @@ class AutomationTrackView : public TrackView virtual void dragEnterEvent( QDragEnterEvent * _dee ); virtual void dropEvent( QDropEvent * _de ); + virtual void updateTrackOperationsWidgetMenu (TrackOperationsWidget *trackOperations) override; + +public slots: + void recordingOn(); + void recordingOff(); } ; diff --git a/include/BBTrack.h b/include/BBTrack.h index a906b54d259..b63cac45791 100644 --- a/include/BBTrack.h +++ b/include/BBTrack.h @@ -135,7 +135,7 @@ class LMMS_EXPORT BBTrack : public Track virtual bool play( const MidiTime & _start, const fpp_t _frames, const f_cnt_t _frame_base, int _tco_num = -1 ); virtual TrackView * createView( TrackContainerView* tcv ); - virtual TrackContentObject * createTCO( const MidiTime & _pos ); + virtual TrackContentObject * unsafeCreateTCO(const MidiTime &_pos); virtual void saveTrackSpecificSettings( QDomDocument & _doc, QDomElement & _parent ); diff --git a/include/Engine.h b/include/Engine.h index 18960ec8f4c..a93ced0d506 100644 --- a/include/Engine.h +++ b/include/Engine.h @@ -31,6 +31,7 @@ #include "lmms_export.h" +#include "lmms_basics.h" class BBTrackContainer; class DummyTrackContainer; @@ -100,6 +101,9 @@ class LMMS_EXPORT LmmsCore : public QObject { return s_framesPerTick; } + + static float framesPerTick (sample_rate_t sample_rate); + static void updateFramesPerTick(); static inline LmmsCore * inst() diff --git a/include/EnvelopeAndLfoParameters.h b/include/EnvelopeAndLfoParameters.h index 4824062f3be..ffeebdbd94e 100644 --- a/include/EnvelopeAndLfoParameters.h +++ b/include/EnvelopeAndLfoParameters.h @@ -163,6 +163,7 @@ public slots: sample_t m_random; bool m_bad_lfoShapeData; SampleBuffer m_userWave; + SampleBuffer::InfoUpdatingValue m_userWaveInfo; enum LfoShapes { diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h index c487438d0f8..3a394ce8b32 100644 --- a/include/InstrumentTrack.h +++ b/include/InstrumentTrack.h @@ -136,7 +136,7 @@ class LMMS_EXPORT InstrumentTrack : public Track, public MidiEventProcessor virtual TrackView * createView( TrackContainerView* tcv ); // create new track-content-object = pattern - virtual TrackContentObject * createTCO( const MidiTime & _pos ); + virtual TrackContentObject * unsafeCreateTCO(const MidiTime &_pos) override ; // called by track @@ -319,6 +319,8 @@ class InstrumentTrackView : public TrackView QMenu * createFxMenu( QString title, QString newFxLabel ); + virtual void updateTrackOperationsWidgetMenu (TrackOperationsWidget *trackOperations) override; + protected: virtual void dragEnterEvent( QDragEnterEvent * _dee ); virtual void dropEvent( QDropEvent * _de ); diff --git a/include/LfoController.h b/include/LfoController.h index 9dfbba6715a..432af1897b9 100644 --- a/include/LfoController.h +++ b/include/LfoController.h @@ -77,6 +77,7 @@ public slots: private: SampleBuffer * m_userDefSampleBuffer; + SampleBuffer::InfoUpdatingValue m_userDefSampleBufferInfo; protected slots: void updatePhase(); diff --git a/include/MemoryManager.h b/include/MemoryManager.h index 4c225e026a9..3fdf5012ca8 100644 --- a/include/MemoryManager.h +++ b/include/MemoryManager.h @@ -46,8 +46,11 @@ class LMMS_EXPORT MemoryManager }; template -struct MmAllocator +class MmAllocator { +public: + MmAllocator() = default; + template MmAllocator(const MmAllocator& other) {} typedef T value_type; template struct rebind { typedef MmAllocator other; }; @@ -65,6 +68,18 @@ struct MmAllocator }; +template< class T1, class T2 > +constexpr bool operator==( const MmAllocator&, const MmAllocator&) +{ + return true; +} + +template< class T1, class T2 > +constexpr bool operator!=( const MmAllocator&, const MmAllocator&) +{ + return false; +} + #define MM_OPERATORS \ public: \ static void * operator new ( size_t size ) \ diff --git a/include/MemoryUtils.h b/include/MemoryUtils.h new file mode 100644 index 00000000000..1c9bf3aff28 --- /dev/null +++ b/include/MemoryUtils.h @@ -0,0 +1,36 @@ +#ifndef MEMORYUTILS_H +#define MEMORYUTILS_H + +#include + +#include + +namespace internal{ + void qobjectDeleter(QObject *object); + + struct QObjectDeleter { + void operator () (QObject *object) + { + object->deleteLater(); + } + }; +} + +template +std::shared_ptr +makeSharedQObject(Args&&...args) +{ + return std::shared_ptr(new T(std::forward(args)...), internal::qobjectDeleter); +} + +template +using UniqueQObjectPointer=std::unique_ptr; + +template +UniqueQObjectPointer +makeUniqueQObject(Args&&...args) +{ + return UniqueQObjectPointer(new T(std::forward(args)...), internal::QObjectDeleter{}); +} + +#endif // MEMORYUTILS_H diff --git a/include/Mixer.h b/include/Mixer.h index e91cd15c25b..06200706f14 100644 --- a/include/Mixer.h +++ b/include/Mixer.h @@ -69,7 +69,47 @@ class MixerWorkerThread; class LMMS_EXPORT Mixer : public QObject { Q_OBJECT + public: + /** + * @brief RAII helper for requestChangesInModel. + * Used by Mixer::requestChangesGuard. + */ + class RequestChangesGuard { + friend class Mixer; + + private: + RequestChangesGuard(Mixer *mixer) + : m_mixer{mixer} + { + m_mixer->requestChangeInModel(); + } + public: + + RequestChangesGuard() + : m_mixer{nullptr} + { + } + + RequestChangesGuard(RequestChangesGuard &&other) + : RequestChangesGuard() + { + std::swap(other.m_mixer, m_mixer); + } + + // Disallow copy. + RequestChangesGuard(const RequestChangesGuard&) = delete; + RequestChangesGuard& operator =(const RequestChangesGuard&) = delete; + + ~RequestChangesGuard() { + if (m_mixer) + m_mixer->doneChangeInModel(); + } + + private: + Mixer *m_mixer; + }; + struct qualitySettings { enum Mode @@ -290,7 +330,7 @@ class LMMS_EXPORT Mixer : public QObject return m_fifoWriter != NULL; } - void pushInputFrames( sampleFrame * _ab, const f_cnt_t _frames ); + void pushInputFrames(const sampleFrame *_ab, const f_cnt_t _frames); inline const sampleFrame * inputBuffer() { @@ -315,6 +355,17 @@ class LMMS_EXPORT Mixer : public QObject void requestChangeInModel(); void doneChangeInModel(); + RequestChangesGuard requestChangesGuard() + { + return RequestChangesGuard{this}; + } + + template()())> + ReturnT runWhileNotRendering(Callable &&callable) { + auto guard = this->requestChangesGuard(); + return callable(); + } + static bool isAudioDevNameValid(QString name); static bool isMidiDevNameValid(QString name); diff --git a/include/Oscillator.h b/include/Oscillator.h index 408e69dbc6c..c4dd130aa90 100644 --- a/include/Oscillator.h +++ b/include/Oscillator.h @@ -35,6 +35,7 @@ #include "SampleBuffer.h" #include "lmms_constants.h" +#include "lmms_math.h" class IntModel; diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 26dda70a9fe..e4c432bacd4 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -26,290 +26,383 @@ #ifndef SAMPLE_BUFFER_H #define SAMPLE_BUFFER_H -#include -#include - -#include - -#include "lmms_export.h" -#include "interpolation.h" -#include "lmms_basics.h" -#include "lmms_math.h" -#include "shared_object.h" -#include "MemoryManager.h" - +#include "internal/SampleBufferData.h" +#include "internal/SampleBufferPlayInfo.h" +#include "JournallingObject.h" +#include "Threading.h" +#include "UpdatingValue.h" class QPainter; + class QRect; // values for buffer margins, used for various libsamplerate interpolation modes // the array positions correspond to the converter_type parameter values in libsamplerate // if there appears problems with playback on some interpolation mode, then the value for that mode // may need to be higher - conversely, to optimize, some may work with lower values -const f_cnt_t MARGIN[] = { 64, 64, 64, 4, 4 }; +const f_cnt_t MARGIN[] = {64, 64, 64, 4, 4}; -class LMMS_EXPORT SampleBuffer : public QObject, public sharedObject -{ - Q_OBJECT - MM_OPERATORS -public: - enum LoopMode { - LoopOff = 0, - LoopOn, - LoopPingPong +class LMMS_EXPORT SampleBuffer : public QObject, public JournallingObject { +Q_OBJECT +MM_OPERATORS + + enum UpdateType : int { + Clear, + Append + }; + + typedef std::shared_ptr SharedData; + typedef std::shared_ptr SharedPlayInfo; + + template> + class LMMS_EXPORT _MixerGuardedSharedPtr { + public: + explicit _MixerGuardedSharedPtr(PtrType ptr) + : m_ptr(std::move(ptr)) { + } + + VarType *operator->() { + return m_ptr.get(); + } + + VarType &operator*() { + return (*m_ptr); + } + + + private: + PtrType m_ptr; + Mixer::RequestChangesGuard m_guard = Engine::mixer()->requestChangesGuard(); + }; + + using GuardedPlayInfo=_MixerGuardedSharedPtr; + using GuardedConstPlayInfo=_MixerGuardedSharedPtr; + + GuardedPlayInfo guardedPlayInfo() { + return GuardedPlayInfo(m_playInfo); + } + + GuardedConstPlayInfo guardedPlayInfo() const { + return GuardedConstPlayInfo(m_playInfo); + } + + + using GuardedData=_MixerGuardedSharedPtr; + using GuardedConstData=_MixerGuardedSharedPtr; + + GuardedData guardedData() { + return GuardedData(m_data); + } + + GuardedConstData guardedData() const { + return GuardedConstData(m_data); + } + + struct LMMS_EXPORT SampleBufferInfo { + SampleBufferInfo() = default; + + SampleBufferInfo(GuardedPlayInfo &playInfo, GuardedData &data) + : startFrame{playInfo->getStartFrame()}, + endFrame{playInfo->getEndFrame()}, + loopStartFrame{playInfo->getLoopStartFrame()}, + loopEndFrame{playInfo->getLoopEndFrame()}, + + amplification{data->getAmplification()}, + audioFile(playInfo->getMaybeAudioFile()), + frames{data->frames()}, + frequency{data->getFrequency()}, + sampleRate{data->getSampleRate()} { + } + + int sampleLength() const { + double playFrames = double(endFrame - startFrame); + + return playFrames / sampleRate * 1000; + } + + f_cnt_t startFrame; + f_cnt_t endFrame; + f_cnt_t loopStartFrame; + f_cnt_t loopEndFrame; + + float amplification; + QString audioFile; + f_cnt_t frames; + float frequency; + sample_rate_t sampleRate; }; - class LMMS_EXPORT handleState - { - MM_OPERATORS + +public: + typedef UpdatingValue InfoUpdatingValue; + + SampleBufferInfo createInfo() { + auto playInfo = guardedPlayInfo(); + auto data = guardedData(); + + return SampleBufferInfo(playInfo, data); + } + + InfoUpdatingValue createUpdatingValue(QObject *parent) { + return InfoUpdatingValue{*m_infoChangeNotifier, createInfo(), parent}; + } + + using DataVector=internal::SampleBufferData::DataVector; + using LoopMode=::LoopMode; + + class LMMS_EXPORT handleState { + MM_OPERATORS + public: - handleState( bool _varying_pitch = false, int interpolation_mode = SRC_LINEAR ); + handleState(bool _varying_pitch = false, int interpolation_mode = SRC_LINEAR); + virtual ~handleState(); - const f_cnt_t frameIndex() const - { + f_cnt_t frameIndex() const { return m_frameIndex; } - void setFrameIndex( f_cnt_t _index ) - { + void setFrameIndex(f_cnt_t _index) { m_frameIndex = _index; } - bool isBackwards() const - { + bool isBackwards() const { return m_isBackwards; } - void setBackwards( bool _backwards ) - { + void setBackwards(bool _backwards) { m_isBackwards = _backwards; } - - int interpolationMode() const - { + + int interpolationMode() const { return m_interpolationMode; } - private: f_cnt_t m_frameIndex; const bool m_varyingPitch; bool m_isBackwards; - SRC_STATE * m_resamplingData; + SRC_STATE *m_resamplingData; int m_interpolationMode; + }; - friend class SampleBuffer; - } ; + explicit SampleBuffer(); + explicit SampleBuffer(internal::SampleBufferData &&data); - SampleBuffer(); - // constructor which either loads sample _audio_file or decodes - // base64-data out of string - SampleBuffer( const QString & _audio_file, bool _is_base64_data = false ); - SampleBuffer( const sampleFrame * _data, const f_cnt_t _frames ); - explicit SampleBuffer( const f_cnt_t _frames ); + /** + * Load this sample buffer from @a _audio_file, + * @param _audio_file path to an audio file. + */ + explicit SampleBuffer(const QString &_audio_file, bool ignoreError=false); - virtual ~SampleBuffer(); + /** + * Load the sample buffer from a base64 encoded string. + * @param base64Data The data. + * @param sample_rate The data's sample rate. + */ + SampleBuffer(const QString &base64Data, sample_rate_t sample_rate); - bool play( sampleFrame * _ab, handleState * _state, - const fpp_t _frames, - const float _freq, - const LoopMode _loopmode = LoopOff ); + SampleBuffer(DataVector &&movedData, sample_rate_t sampleRate); - void visualize( QPainter & _p, const QRect & _dr, const QRect & _clip, f_cnt_t _from_frame = 0, f_cnt_t _to_frame = 0 ); - inline void visualize( QPainter & _p, const QRect & _dr, f_cnt_t _from_frame = 0, f_cnt_t _to_frame = 0 ) - { - visualize( _p, _dr, _dr, _from_frame, _to_frame ); + inline virtual QString nodeName() const override { + return "samplebuffer"; } - inline const QString & audioFile() const - { - return m_audioFile; - } + SampleBuffer(SampleBuffer &&sampleBuffer) noexcept; - inline f_cnt_t startFrame() const - { - return m_startFrame; - } + SampleBuffer &operator=(SampleBuffer &&other) noexcept; - inline f_cnt_t endFrame() const - { - return m_endFrame; - } + virtual void saveSettings(QDomDocument &doc, QDomElement &_this) override; - inline f_cnt_t loopStartFrame() const - { - return m_loopStartFrame; - } + virtual void loadSettings(const QDomElement &_this) override; - inline f_cnt_t loopEndFrame() const - { - return m_loopEndFrame; - } + bool play(sampleFrame *_ab, handleState *_state, + const fpp_t _frames, + const float _freq, + const LoopMode _loopmode = LoopMode::LoopOff); - void setLoopStartFrame( f_cnt_t _start ) - { - m_loopStartFrame = _start; + template + void setLoopStartFrame(f_cnt_t _start) { + runToSetPlayInfo([=](GuardedPlayInfo &playInfo) { + playInfo->setLoopEndFrame(_start); + }); } - void setLoopEndFrame( f_cnt_t _end ) - { - m_loopEndFrame = _end; + template + void setLoopEndFrame(f_cnt_t _end) { + runToSetPlayInfo([=](GuardedPlayInfo &playInfo) { + playInfo->setLoopEndFrame(_end); + }); } - void setAllPointFrames( f_cnt_t _start, f_cnt_t _end, f_cnt_t _loopstart, f_cnt_t _loopend ) - { - m_startFrame = _start; - m_endFrame = _end; - m_loopStartFrame = _loopstart; - m_loopEndFrame = _loopend; - } - inline f_cnt_t frames() const - { - return m_frames; + template + void setAllPointFrames(f_cnt_t _start, f_cnt_t _end, f_cnt_t _loopstart, f_cnt_t _loopend) { + runToSetPlayInfo([=](GuardedPlayInfo &playInfo) { + playInfo->setStartFrame(_start); + playInfo->setEndFrame(_end); + playInfo->setLoopStartFrame(_loopstart); + playInfo->setLoopEndFrame(_loopend); + }); } - inline float amplification() const - { - return m_amplification; + template + void setStartFrame(const f_cnt_t _s) { + runToSetPlayInfo([=](GuardedPlayInfo &playInfo) { + playInfo->setStartFrame(_s); + }); } - inline bool reversed() const - { - return m_reversed; + template + void setEndFrame(const f_cnt_t _e) { + runToSetPlayInfo([=](GuardedPlayInfo &playInfo) { + playInfo->setEndFrame(_e); + }); } - inline float frequency() const - { - return m_frequency; + template + void setAmplification(float _a) { + runToSetData([_a](GuardedData &data) { + data->setAmplification(_a); + }); } - sample_rate_t sampleRate() const - { - return m_sampleRate; + template + void setFrequency(float frequency) { + runToSetData([frequency](GuardedData &data) { + data->setFrequency(frequency); + }); } - int sampleLength() const - { - return double( m_endFrame - m_startFrame ) / m_sampleRate * 1000; - } + void setAudioFile(const QString &audioFile, bool ignoreError = false); - inline void setFrequency( float _freq ) - { - m_frequency = _freq; - } + static QString openAudioFile(const QString ¤tAudioFile = QString()); - inline void setSampleRate( sample_rate_t _rate ) - { - m_sampleRate = _rate; - } + QString openAndSetAudioFile(const QString ¤tAudioFile = QString()); - inline const sampleFrame * data() const - { - return m_data; - } + QString openAndSetWaveformFile(QString currentAudioFile = QString()); - QString openAudioFile() const; - QString openAndSetAudioFile(); - QString openAndSetWaveformFile(); + sample_t userWaveSample(const float _sample) const; - QString & toBase64( QString & _dst ) const; + static QString tryToMakeRelative(const QString &_file); - // protect calls from the GUI to this function with dataReadLock() and - // dataUnlock() - SampleBuffer * resample( const sample_rate_t _src_sr, - const sample_rate_t _dst_sr ); + static QString tryToMakeAbsolute(const QString &file); - void normalizeSampleRate( const sample_rate_t _src_sr, - bool _keep_settings = false ); + /** + * @brief Add data to the buffer, + * @param begin Beginning of an InputIterator. + * @param end End of an InputIterator. + * @return + */ + void addData(const DataVector &vector, sample_rate_t sampleRate); - // protect calls from the GUI to this function with dataReadLock() and - // dataUnlock(), out of loops for efficiency - inline sample_t userWaveSample( const float _sample ) const - { - f_cnt_t frames = m_frames; - sampleFrame * data = m_data; - const float frame = _sample * frames; - f_cnt_t f1 = static_cast( frame ) % frames; - if( f1 < 0 ) - { - f1 += frames; - } - return linearInterpolate( data[f1][0], data[ (f1 + 1) % frames ][0], fraction( frame ) ); - } + /** + * @brief Reset the class and initialize it with @a newData. + * @param newData mm, that's the new data. + * @param dataSampleRate Sample rate for @a newData. + */ + void resetData(DataVector &&newData, sample_rate_t dataSampleRate); + + /** + * @brief Just reverse the current buffer. + * + * This function simply calls `std::reverse` on m_data. + */ + void reverse(); + + void visualize(QPainter &_p, const QRect &_dr, const QRect &_clip, f_cnt_t _from_frame = 0, f_cnt_t _to_frame = 0); - void dataReadLock() - { - m_varLock.lockForRead(); + inline void visualize(QPainter &_p, const QRect &_dr, f_cnt_t _from_frame = 0, f_cnt_t _to_frame = 0) { + visualize(_p, _dr, _dr, _from_frame, _to_frame); } - void dataUnlock() - { - m_varLock.unlock(); + std::pair + visualizeToPoly(const QRect &_dr, const QRect &_clip, f_cnt_t _from_frame = 0, f_cnt_t _to_frame = 0) const; + + +private: + + QString &toBase64(QString &_dst) const; + + // HACK: libsamplerate < 0.1.8 doesn't get read-only variables + // as const. It has been fixed in 0.1.9 but has not been + // shipped for some distributions. + // This function just returns a variable that should have + // been `const` as non-const to bypass using 0.1.9. + inline static sampleFrame *libSampleRateSrc(const sampleFrame *ptr) { + return const_cast(ptr); } - static QString tryToMakeRelative( const QString & _file ); - static QString tryToMakeAbsolute(const QString & file); + static f_cnt_t getLoopedIndex(f_cnt_t _index, f_cnt_t _startf, f_cnt_t _endf); + static f_cnt_t getPingPongIndex(f_cnt_t _index, f_cnt_t _startf, f_cnt_t _endf); -public slots: - void setAudioFile( const QString & _audio_file ); - void loadFromBase64( const QString & _data ); - void setStartFrame( const f_cnt_t _s ); - void setEndFrame( const f_cnt_t _e ); - void setAmplification( float _a ); - void setReversed( bool _on ); - void sampleRateChanged(); + /** + * @brief A helper class used when changes to m_data is needed + */ + class DataChangeHelper { + public: + explicit DataChangeHelper(SampleBuffer *buffer, UpdateType updateType); -private: - void update( bool _keep_settings = false ); - - void convertIntToFloat ( int_sample_t * & _ibuf, f_cnt_t _frames, int _channels); - void directFloatWrite ( sample_t * & _fbuf, f_cnt_t _frames, int _channels); - - f_cnt_t decodeSampleSF( QString _f, sample_t * & _buf, - ch_cnt_t & _channels, - sample_rate_t & _sample_rate ); -#ifdef LMMS_HAVE_OGGVORBIS - f_cnt_t decodeSampleOGGVorbis( QString _f, int_sample_t * & _buf, - ch_cnt_t & _channels, - sample_rate_t & _sample_rate ); -#endif - f_cnt_t decodeSampleDS( QString _f, int_sample_t * & _buf, - ch_cnt_t & _channels, - sample_rate_t & _sample_rate ); - - QString m_audioFile; - sampleFrame * m_origData; - f_cnt_t m_origFrames; - sampleFrame * m_data; - QReadWriteLock m_varLock; - f_cnt_t m_frames; - f_cnt_t m_startFrame; - f_cnt_t m_endFrame; - f_cnt_t m_loopStartFrame; - f_cnt_t m_loopEndFrame; - float m_amplification; - bool m_reversed; - float m_frequency; - sample_rate_t m_sampleRate; - - sampleFrame * getSampleFragment( f_cnt_t _index, f_cnt_t _frames, - LoopMode _loopmode, - sampleFrame * * _tmp, - bool * _backwards, f_cnt_t _loopstart, f_cnt_t _loopend, - f_cnt_t _end ) const; - f_cnt_t getLoopedIndex( f_cnt_t _index, f_cnt_t _startf, f_cnt_t _endf ) const; - f_cnt_t getPingPongIndex( f_cnt_t _index, f_cnt_t _startf, f_cnt_t _endf ) const; + ~DataChangeHelper(); + private: + SampleBuffer *m_buffer; + UpdateType m_updateType; + Mixer::RequestChangesGuard m_mixerGuard; + }; + + /** + * A little helper that will run a callable + * with async, and giving it a guarded play + * info as an argument and notifies about the + * changes in the end. + */ + template + void runToSetValue(Func &&func) { + runAccordingToLaunchType([func, this] { + func(); + m_infoChangeNotifier->onValueUpdated(createInfo()); + }, LaunchType{}); + } + + /** + * Run a function and give it a GuardedPlayInfo as a parameter. + * @param func The function to run. + */ + template + void runToSetPlayInfo(Func &&func) { + runToSetValue([func, this] { + auto playInfo = guardedPlayInfo(); + func(playInfo); + }); + } + + /** + * Run a function and give it a GuardedData as a parameter. + * @param func The function to run. + */ + template + void runToSetData(Func &&func) { + runToSetValue([func, this] { + auto data = guardedData(); + func(data); + }); + } signals: - void sampleUpdated(); -} ; + void sampleUpdated(int updateType); + +private: + SharedData m_data; + SharedPlayInfo m_playInfo; + + UniqueQObjectPointer m_infoChangeNotifier = + makeUniqueQObject(); +}; #endif diff --git a/include/SampleBufferVisualizer.h b/include/SampleBufferVisualizer.h new file mode 100644 index 00000000000..1b47245e27d --- /dev/null +++ b/include/SampleBufferVisualizer.h @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2018 Shmuel H. (shmuelhazan0/at/gmail.com) + * + * This file is part of LMMS - https://lmms.io + * + * 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 2 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef SAMPLEBUFFERVISUALIZER_H +#define SAMPLEBUFFERVISUALIZER_H + +#include +#include + +#include "MidiTime.h" +#include "SampleBuffer.h" + + +/** + * @brief A caching implementation of a visualizer for SampleBuffer. + * + * This implementation splits the tco into fragments of + * 192 ticks. Then, it draws them into @a PixmapCacheLine. + */ +class SampleBufferVisualizer +{ + struct PixmapCacheLine + { + /** + * @brief The visualization of this cache line. + */ + QPixmap pixmap; + + /** + * @brief The rectangle of @a pixmap. + */ + QRect rect; + + /** + * @brief How much time @a pixmap is representing. + */ + MidiTime totalTime; + }; + +public: + SampleBufferVisualizer(); + + using Operation=SampleBuffer::UpdateType; + + /** + * @brief Update the cache before drawing it. + * @param sampleBuffer The sample buffer to visualize. + * @param sampleStartOffset Offset from the sample buffer's beginning. + * @param sampleLength The time we should + * visualize (can be different that the sample buffer's length). + * @param parentRect The rectangle we should paint in. + * @param pixelsPerTact Current value of pixels per tact. + * @param framesPerTact Current value of frames per tact. + * @param pen The pen setting we should paint with. + * @param operation Should we clear the cache or just append in the end? + */ + void update(const SampleBuffer &sampleBuffer, + MidiTime sampleStartOffset, + MidiTime sampleLength, + const QRect &parentRect, + float pixelsPerTact, f_cnt_t framesPerTact, + const QPen &pen, + Operation operation); + + /** + * @brief draw Draw the current cache into @a painter. + */ + void draw(QPainter &painter); + +private: + /** + * @brief appendMultipleTacts Add one or more cache lines or + * just add data to an existing + * cache line. + * @param sampleBuffer The sample buffer to visualize. + * @param sampleLength Time to visualize. + * @param parentRect The rectangle we should paint in. + * @param pen The pen setting we should paint with. + */ + void appendMultipleTacts(const SampleBuffer &sampleBuffer, + MidiTime sampleLength, + const QRect &parentRect, const QPen &pen); + + /** + * @brief appendTact Add data to m_currentPixmap. + * @param sampleBuffer The sample buffer to visualize. + * @param totalTime How much time we should visualize from + * m_currentPixmap.totalTime. + * @param parentRect The rectangle we should paint in. + * @param pen + * @param pen The pen setting we should paint with. + * @return true if we have painted at least one + * pixel; false otherwise. + */ + bool appendTact(const SampleBuffer &sampleBuffer, + const MidiTime &totalTime, + const QRect &parentRect, const QPen &pen, bool isLastInTact); + + /** + * @brief getRectForSampleFragment Construct a rectangle for + * visualization of a given + * MidiTime range. + * @param parentRect The rectangle we should be in. + * @param beginOffset Offset of the rectangle from @a + * parentRect's left. + * @param totalTime The actual time to be painted. + * @param forceNotZeroWidth Should we make sure rect.width != 0? + */ + QRect getRectForSampleFragment(QRect parentRect, + MidiTime beginOffset, + MidiTime totalTime, + bool forceNotZeroWidth=false); + + /** + * @brief pixelsPerTime Calculate how much pixels a visualization of + * @a time would take. + */ + float pixelsPerTime (const MidiTime &time) + { + return ((time * m_pixelsPerTact) / MidiTime::ticksPerTact()); + } + + /** + * How much time have we cached already (not including + * the cache in m_currentPixmap). + */ + MidiTime m_cachedTime{0}; + + /** + * X time drawing offset from the beginning. + */ + MidiTime m_generalPaintOffset{0}; + /** + * X pixel drawing offset from the beginning. + */ + int m_drawPixelOffset = 0; + + QList m_cachedPixmaps; + + /** + * The current cache line we've not finished yet. + */ + PixmapCacheLine m_currentPixmap; + + float m_pixelsPerTact = 0; + f_cnt_t m_framesPerTact = 0; +}; + +#endif // SAMPLEBUFFERVISUALIZER_H diff --git a/include/SamplePlayHandle.h b/include/SamplePlayHandle.h index d10c448378c..f167db3a58a 100644 --- a/include/SamplePlayHandle.h +++ b/include/SamplePlayHandle.h @@ -30,6 +30,8 @@ #include "AutomatableModel.h" #include "PlayHandle.h" +#include + class BBTrack; class SampleTCO; class Track; @@ -39,7 +41,7 @@ class AudioPort; class SamplePlayHandle : public PlayHandle { public: - SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPort = true ); + SamplePlayHandle( const std::shared_ptr &sampleBuffer, bool ownAudioPort = true ); SamplePlayHandle( const QString& sampleFile ); SamplePlayHandle( SampleTCO* tco ); virtual ~SamplePlayHandle(); @@ -77,7 +79,8 @@ class SamplePlayHandle : public PlayHandle private: - SampleBuffer * m_sampleBuffer; + std::shared_ptr m_sampleBuffer; + SampleBuffer::SampleBufferInfo m_sampleBufferInfo; bool m_doneMayReturnTrue; f_cnt_t m_frame; diff --git a/include/SampleRecordHandle.h b/include/SampleRecordHandle.h index 22d9bf3156c..563ca46bb77 100644 --- a/include/SampleRecordHandle.h +++ b/include/SampleRecordHandle.h @@ -28,12 +28,15 @@ #include #include +#include #include "MidiTime.h" #include "PlayHandle.h" +#include "SampleTrack.h" +#include "SampleBuffer.h" + class BBTrack; -class SampleBuffer; class SampleTCO; class Track; @@ -41,7 +44,7 @@ class Track; class SampleRecordHandle : public PlayHandle { public: - SampleRecordHandle( SampleTCO* tco ); + SampleRecordHandle( SampleTCO* tco , MidiTime startRecordTimeOffset); virtual ~SampleRecordHandle(); virtual void play( sampleFrame * _working_buffer ); @@ -50,22 +53,46 @@ class SampleRecordHandle : public PlayHandle virtual bool isFromTrack( const Track * _track ) const; f_cnt_t framesRecorded() const; - void createSampleBuffer( SampleBuffer * * _sample_buf ); private: + void copyBufferFromMonoLeft( const sampleFrame * inputBuffer, + sampleFrame *outputBuffer, + const f_cnt_t _frames); + void copyBufferFromMonoRight( const sampleFrame * inputBuffer, + sampleFrame *outputBuffer, + const f_cnt_t _frames); + void copyBufferFromStereo( const sampleFrame * inputBuffer, + sampleFrame *outputBuffer, + const f_cnt_t _frames); + + void addOrCreateBuffer(); + + virtual void writeBuffer( const sampleFrame * _ab, const f_cnt_t _frames ); - typedef QList > bufferList; - bufferList m_buffers; f_cnt_t m_framesRecorded; - MidiTime m_minLength; + + /** + * @brief Total of ticks we've recorded. + */ + MidiTime m_timeRecorded; + + SampleBuffer::DataVector m_currentBuffer; + QFuture m_lastAsyncWork; Track * m_track; BBTrack * m_bbTrack; SampleTCO * m_tco; + // The recording type as it was when we started + // recording. + SampleTrack::RecordingChannel m_recordingChannel; + + // The offset from the start of m_track that the record has + // started from. + MidiTime m_startRecordTimeOffset; } ; diff --git a/include/SampleTrack.h b/include/SampleTrack.h index ccb5a020e85..bf48cdd853c 100644 --- a/include/SampleTrack.h +++ b/include/SampleTrack.h @@ -32,6 +32,7 @@ #include "FxMixer.h" #include "FxLineLcdSpinBox.h" #include "Track.h" +#include "SampleBufferVisualizer.h" class EffectRackView; class Knob; @@ -59,40 +60,51 @@ class SampleTCO : public TrackContentObject return "sampletco"; } - SampleBuffer* sampleBuffer() - { + std::shared_ptr sampleBuffer() { return m_sampleBuffer; } MidiTime sampleLength() const; - void setSampleStartFrame( f_cnt_t startFrame ); - void setSamplePlayLength( f_cnt_t length ); - virtual TrackContentObjectView * createView( TrackView * _tv ); + + virtual TrackContentObjectView *createView(TrackView *_tv); bool isPlaying() const; + void setIsPlaying(bool isPlaying); + /** + * @brief isEmpty Check if this TCO has not content. + */ + bool isEmpty() const; + public slots: - void setSampleBuffer( SampleBuffer* sb ); void setSampleFile( const QString & _sf ); void updateLength(); void toggleRecord(); - void playbackPositionChanged(); - void updateTrackTcos(); +private slots: + + void onSampleBufferChanged(int updateType); + private: - SampleBuffer* m_sampleBuffer; + std::shared_ptr m_sampleBuffer; + SampleBuffer::InfoUpdatingValue m_sampleBufferInfo; BoolModel m_recordModel; bool m_isPlaying; + using Watcher=QFutureWatcher; + + Watcher m_loadingWatcher; friend class SampleTCOView; + void onFileLoadingFinished(); + signals: - void sampleChanged(); + void sampleChanged(int updateType); } ; @@ -107,8 +119,8 @@ class SampleTCOView : public TrackContentObjectView virtual ~SampleTCOView() = default; public slots: - void updateSample(); + void updateSample(int updateType); protected: @@ -123,23 +135,35 @@ public slots: private: SampleTCO * m_tco; - QPixmap m_paintPixmap; -} ; + std::shared_ptr m_sampleBufferVisualizerMutex = std::make_shared(); + std::shared_ptr m_sampleBufferVisualizer = std::make_shared(); + + using Watcher=QFutureWatcher; + Watcher m_visualizationWatcher; + void updateVisualizer(QPen p, SampleBufferVisualizer::Operation operation); +}; class SampleTrack : public Track { Q_OBJECT + mapPropertyFromModel(bool,isRecord,setRecord,m_recordModel); public: + enum RecordingChannel : int { + None, + MonoRight, + MonoLeft, + Stereo, + }; + SampleTrack( TrackContainer* tc ); virtual ~SampleTrack(); - - virtual bool play( const MidiTime & _start, const fpp_t _frames, - const f_cnt_t _frame_base, int _tco_num = -1 ); - virtual TrackView * createView( TrackContainerView* tcv ); - virtual TrackContentObject * createTCO( const MidiTime & _pos ); + virtual bool play(const MidiTime &_start, const fpp_t _frames, + const f_cnt_t _frame_base, int _tco_num = -1); + virtual TrackView *createView(TrackContainerView *tcv); + virtual TrackContentObject *unsafeCreateTCO(const MidiTime &); virtual void saveTrackSpecificSettings( QDomDocument & _doc, @@ -161,23 +185,31 @@ class SampleTrack : public Track return "sampletrack"; } + RecordingChannel recordingChannel() const; + void setRecordingChannel(const RecordingChannel &recordingChannel); + public slots: void updateTcos(); void setPlayingTcos( bool isPlaying ); void updateEffectChannel(); + void beforeRecordOn (MidiTime time); + void toggleRecord(); + void playbackPositionChanged(); private: + IntModel m_recordingChannelModel; + + BoolModel m_recordModel; FloatModel m_volumeModel; FloatModel m_panningModel; IntModel m_effectChannelModel; AudioPort m_audioPort; - friend class SampleTrackView; friend class SampleTrackWindow; -} ; +}; @@ -207,6 +239,8 @@ class SampleTrackView : public TrackView virtual QMenu * createFxMenu( QString title, QString newFxLabel ); + virtual void updateTrackOperationsWidgetMenu (TrackOperationsWidget *trackOperations) override; + public slots: void showEffects(); @@ -220,11 +254,14 @@ public slots: private slots: + void onRecordActionSelected (QAction *action); void assignFxLine( int channelIndex ); void createFxLine(); private: + QAction *m_toggleRecordAction; + EffectRackView * m_effectRack; SampleTrackWindow * m_window; Knob * m_volumeKnob; Knob * m_panningKnob; @@ -290,7 +327,7 @@ public slots: EffectRackView * m_effectRack; -} ; +}; #endif diff --git a/include/Song.h b/include/Song.h index d88a59e2b40..893c4258720 100644 --- a/include/Song.h +++ b/include/Song.h @@ -342,6 +342,8 @@ public slots: void addBBTrack(); +signals: + void beforeRecordOn (MidiTime time); private slots: void insertBar(); diff --git a/include/SongEditor.h b/include/SongEditor.h index f9b6aad1b56..cd460cde98d 100644 --- a/include/SongEditor.h +++ b/include/SongEditor.h @@ -32,6 +32,7 @@ #include "ActionGroup.h" #include "Editor.h" #include "TrackContainerView.h" +#include "SampleTrack.h" class QLabel; class QScrollBar; @@ -157,6 +158,8 @@ class SongEditorWindow : public Editor SongEditor* m_editor; + SampleTrack::RecordingChannel globalRecordChannel() const; + protected: virtual void resizeEvent( QResizeEvent * event ); virtual void changeEvent( QEvent * ); @@ -170,6 +173,8 @@ protected slots: void lostFocus(); void adjustUiAfterProjectLoad(); + void onRecordChannelSelected(QAction *action); + signals: void playTriggered(); void resized(); @@ -188,6 +193,8 @@ protected slots: QAction* m_crtlAction; ComboBox * m_zoomingComboBox; + + SampleTrack::RecordingChannel m_globalRecordChannel = SampleTrack::RecordingChannel::Stereo; }; #endif diff --git a/include/Threading.h b/include/Threading.h new file mode 100644 index 00000000000..d3e29e6bfea --- /dev/null +++ b/include/Threading.h @@ -0,0 +1,31 @@ +#ifndef THREADING_H +#define THREADING_H + +#include + +template +inline auto runAsync(Callable &&callable) -> QFuture { + return QtConcurrent::run(std::forward(callable)); +} + +template +inline auto runSync(Callable &&callable) -> decltype(callable()) { + return callable(); +} + +namespace LaunchType { + struct Async {}; + struct Sync{}; +} + +template +inline auto runAccordingToLaunchType(Callable &&callable, LaunchType::Async) -> QFuture { + return runAsync(std::forward(callable)); +} + +template +inline auto runAccordingToLaunchType(Callable &&callable, LaunchType::Sync) -> decltype(callable()) { + return callable(); +} + +#endif // THREADING_H diff --git a/include/Track.h b/include/Track.h index 1267d2ef742..ceaebd014d3 100644 --- a/include/Track.h +++ b/include/Track.h @@ -152,6 +152,9 @@ class TrackContentObject : public Model, public JournallingObject MidiTime startTimeOffset() const; void setStartTimeOffset( const MidiTime &startTimeOffset ); + bool isRecording() const; + void setIsRecording(bool value); + public slots: void copy(); void paste(); @@ -183,6 +186,8 @@ public slots: BoolModel m_soloModel; bool m_autoResize; + bool m_isRecording{false}; + bool m_selectViewOnCreate; friend class TrackContentObjectView; @@ -203,6 +208,7 @@ class TrackContentObjectView : public selectableObject, public ModelView Q_PROPERTY( QColor textBackgroundColor READ textBackgroundColor WRITE setTextBackgroundColor ) Q_PROPERTY( QColor textShadowColor READ textShadowColor WRITE setTextShadowColor ) Q_PROPERTY( QColor BBPatternBackground READ BBPatternBackground WRITE setBBPatternBackground ) + Q_PROPERTY( QColor recordingBackgroundColor READ recordingBackgroundColor WRITE setRecordingBackgroundColor ) Q_PROPERTY( bool gradient READ gradient WRITE setGradient ) public: @@ -223,6 +229,8 @@ class TrackContentObjectView : public selectableObject, public ModelView QColor textBackgroundColor() const; QColor textShadowColor() const; QColor BBPatternBackground() const; + QColor recordingBackgroundColor() const; + bool gradient() const; void setMutedColor( const QColor & c ); void setMutedBackgroundColor( const QColor & c ); @@ -232,11 +240,12 @@ class TrackContentObjectView : public selectableObject, public ModelView void setTextShadowColor( const QColor & c ); void setBBPatternBackground( const QColor & c ); void setGradient( const bool & b ); + void setRecordingBackgroundColor (const QColor & c ); // access needsUpdate member variable bool needsUpdate(); void setNeedsUpdate( bool b ); - + public slots: virtual bool close(); void cut(); @@ -308,6 +317,7 @@ protected slots: QColor m_textBackgroundColor; QColor m_textShadowColor; QColor m_BBPatternBackground; + QColor m_recordingBackgroundColor; bool m_gradient; bool m_needsUpdate; @@ -427,6 +437,8 @@ class TrackOperationsWidget : public QWidget ~TrackOperationsWidget(); + QPushButton *trackOps() const; + protected: virtual void mousePressEvent( QMouseEvent * me ); virtual void paintEvent( QPaintEvent * pe ); @@ -436,9 +448,6 @@ private slots: void cloneTrack(); void removeTrack(); void updateMenu(); - void toggleRecording(bool on); - void recordingOn(); - void recordingOff(); void clearTrack(); private: @@ -504,7 +513,9 @@ class LMMS_EXPORT Track : public Model, public JournallingObject virtual TrackView * createView( TrackContainerView * view ) = 0; - virtual TrackContentObject * createTCO( const MidiTime & pos ) = 0; + virtual TrackContentObject * unsafeCreateTCO( const MidiTime & pos ) = 0; + + TrackContentObject * createTCO(const MidiTime &pos); virtual void saveTrackSpecificSettings( QDomDocument & doc, QDomElement & parent ) = 0; @@ -679,6 +690,8 @@ class TrackView : public QWidget, public ModelView, public JournallingObject // Currently instrument track and sample track supports it virtual QMenu * createFxMenu(QString title, QString newFxLabel); + virtual void updateTrackOperationsWidgetMenu (TrackOperationsWidget *trackOperations); + public slots: virtual bool close(); diff --git a/include/UpdatingValue.h b/include/UpdatingValue.h new file mode 100644 index 00000000000..d40264d793f --- /dev/null +++ b/include/UpdatingValue.h @@ -0,0 +1,140 @@ +#ifndef UPDATINGVALUE_HPP +#define UPDATINGVALUE_HPP + +#include + +#include "MemoryUtils.h" +#include "lmms_export.h" + +namespace internal { + class LMMS_EXPORT SignalArgument { + protected: + virtual ~SignalArgument() {} + }; +} + +Q_DECLARE_METATYPE(std::shared_ptr); + + +namespace internal { + class LMMS_EXPORT UpdatingValueNotifier_Untyped : public QObject { + Q_OBJECT + + signals: + + /** + * @brief Notify the listeners (UpdatingValue instances) that we've + * changed the value. + */ + void rawOnValueUpdated(std::shared_ptr value); + }; + + class LMMS_EXPORT UpdatingValue_Untyped : public QObject { + Q_OBJECT + + protected: + UpdatingValue_Untyped(UpdatingValueNotifier_Untyped ¬ifier, QObject *parent) + : QObject{parent} { + // Run in the parents thread. + this->moveToThread(parent->thread()); + + qRegisterMetaType>(); + + // Notify us about changes. + auto connection = QObject::connect( + ¬ifier, + &UpdatingValueNotifier_Untyped::rawOnValueUpdated, + this, + &UpdatingValue_Untyped::onValueUpdated); + } + + virtual void onValueUpdated(std::shared_ptr value) = 0; + }; + + template + class LMMS_EXPORT UpdatingValueImpl : public internal::UpdatingValue_Untyped { + public: + /** + * @brief Sender of updates for this UpdatingValue. + */ + class Notifier : public internal::UpdatingValueNotifier_Untyped { + friend class UpdatingValueImpl; + + struct TypedSignalArgument : public SignalArgument { + TypedSignalArgument(const T &value) + : value{value} { + } + + const T value; + }; + + public: + void onValueUpdated(const T &newValue) { + auto newArgument = std::make_shared(newValue); + emit rawOnValueUpdated(newArgument); + } + }; + + using TypedSignalArugment=typename Notifier::TypedSignalArgument; + + + UpdatingValueImpl(Notifier ¬ifier, const T &initialValue, QObject *parent) + : UpdatingValue_Untyped{notifier, parent}, + m_ourCopy{std::make_shared(std::move(initialValue))} { + } + + + const T *operator->() const { + return &(get()); + } + + const T &operator*() const { + return get(); + } + + const T &get() const { + return m_ourCopy->value; + } + + private: + std::shared_ptr m_ourCopy; + + private: + void onValueUpdated(std::shared_ptr value) override final { + m_ourCopy = std::static_pointer_cast(std::move(value)); + } + }; +} // namespace internal + +/** + UpdatingValue is a lockless method of getting info + from core components. + + Instead of locking for read and reading the value, + we'll get notified when we have any change in the value. + */ +template +class LMMS_EXPORT UpdatingValue { + typedef internal::UpdatingValueImpl Impl; + +public: + typedef typename Impl::Notifier Notifier; + + UpdatingValue(Notifier ¬ifier, const T &initialValue, QObject *parent) + : m_updatingValueImpl{makeUniqueQObject(notifier, initialValue, parent)} { + } + + const T *operator->() const { + return &(this->operator*()); + } + + const T &operator*() const { + return m_updatingValueImpl->get(); + } + +private: + UniqueQObjectPointer> m_updatingValueImpl; +}; + + +#endif // UPDATINGVALUE_HPP diff --git a/include/internal/SampleBufferData.h b/include/internal/SampleBufferData.h new file mode 100644 index 00000000000..ef55fe5d2aa --- /dev/null +++ b/include/internal/SampleBufferData.h @@ -0,0 +1,112 @@ +#ifndef LMMS_SAMPLEBUFFERDATA_H +#define LMMS_SAMPLEBUFFERDATA_H + +#include +#include +#include "Mixer.h" +#include "Engine.h" +#include "MemoryManager.h" +#include "lmms_basics.h" +#include "lmms_export.h" + +namespace internal { + inline sample_rate_t mixerSampleRate() { + return Engine::mixer()->processingSampleRate(); + } + + class LMMS_EXPORT SampleBufferData { + public: + typedef std::vector> DataVector; + + SampleBufferData(DataVector &&data, + sample_rate_t sampleRate); + + SampleBufferData() = default; + + + static SampleBufferData loadFromBase64(const QString &_data, + sample_rate_t sampleRate); + + f_cnt_t frames() const { + return static_cast(m_data.size()); + } + + void addData(const DataVector &vector, sample_rate_t sampleRate) { + if (sampleRate != m_sampleRate) { + auto resampledVector = resampleData(vector, sampleRate, m_sampleRate); + m_data.insert(m_data.end(), resampledVector.cbegin(), resampledVector.cend()); + } else { + m_data.insert(m_data.end(), vector.cbegin(), vector.cend()); + } + } + + void resetData(DataVector &&newData, sample_rate_t dataSampleRate) { + m_sampleRate = dataSampleRate; + m_data = std::move(newData); + } + + void reverse() { + std::reverse(m_data.begin(), m_data.end()); + } + + static DataVector + resampleData(const DataVector &inputData, sample_rate_t inputSampleRate, sample_rate_t desiredSampleRate); + + const + sampleFrame *getSampleFragment(f_cnt_t _index, + f_cnt_t _frames, LoopMode _loopmode, + sampleFrame **_tmp, + bool *_backwards, + f_cnt_t _loopstart, f_cnt_t _loopend, + f_cnt_t _end) const; + + sampleFrame *data() { + return m_data.data(); + } + + const + sampleFrame *data() const { + return m_data.data(); + } + + private: + // HACK: libsamplerate < 0.1.9's structs does not mark read-only variables + // as const. It has been fixed in 0.1.9 but has not been + // shipped for some distributions. + // This function just returns a variable that should have + // been `const` as non-const. + inline static sampleFrame *libSampleRateSrc(const sampleFrame *ptr) { + return const_cast(ptr); + } + + + DataVector m_data; + float m_frequency = BaseFreq; + float m_amplification = 1.0f; + sample_rate_t m_sampleRate = internal::mixerSampleRate(); + public: + float getFrequency() const { + return m_frequency; + } + + sample_rate_t getSampleRate() const { + return m_sampleRate; + } + + void setFrequency(float frequency) + { + m_frequency = frequency; + } + + float getAmplification() const + { + return m_amplification; + } + + void setAmplification(float amplification) + { + m_amplification = amplification; + } + }; +} +#endif //LMMS_SAMPLEBUFFERDATA_H diff --git a/include/internal/SampleBufferFileHelper.h b/include/internal/SampleBufferFileHelper.h new file mode 100644 index 00000000000..97d0b47bf14 --- /dev/null +++ b/include/internal/SampleBufferFileHelper.h @@ -0,0 +1,27 @@ +#ifndef LMMS_SAMPLEBUFFERFILEHELPER_H +#define LMMS_SAMPLEBUFFERFILEHELPER_H + +#include +#include "SampleBufferData.h" + +namespace internal { + class SampleBufferFileHelper { + public: + using FileName=QString; + + /** + * @brief Load file data + * @param fileName Path to the file. + * @param ignoreError Should we present an error to the user? + */ + static + SampleBufferData Load(FileName fileName, bool ignoreError = false); + + private: + static QString tryToMakeRelative(const QString &file); + + }; +} + + +#endif //LMMS_SAMPLEBUFFERFILEHELPER_H diff --git a/include/internal/SampleBufferPlayInfo.h b/include/internal/SampleBufferPlayInfo.h new file mode 100644 index 00000000000..08a324952b9 --- /dev/null +++ b/include/internal/SampleBufferPlayInfo.h @@ -0,0 +1,83 @@ +// +// Created by reflexe on 26/03/19. +// + +#ifndef LMMS_SAMPLEBUFFERPLAYINFO_H +#define LMMS_SAMPLEBUFFERPLAYINFO_H + +#include "Mixer.h" +#include "lmms_export.h" + +namespace internal { + class LMMS_EXPORT SampleBufferPlayInfo { + public: + explicit SampleBufferPlayInfo(f_cnt_t frames) + : m_endFrame{frames}, m_loopEndFrame{frames} { + } + + int sampleLength(sample_rate_t sampleRate) const { + return static_cast(double(m_endFrame - m_startFrame) / sampleRate * 1000); + } + + f_cnt_t getStartFrame() const { + return m_startFrame; + } + + f_cnt_t getEndFrame() const { + return m_endFrame; + } + + f_cnt_t getLoopStartFrame() const { + return m_loopStartFrame; + } + + f_cnt_t getLoopEndFrame() const { + return m_loopEndFrame; + } + + float getAmplification() const { + return m_amplification; + } + + void setStartFrame(f_cnt_t startFrame) { + m_startFrame = startFrame; + } + + void setEndFrame(f_cnt_t endFrame) { + m_endFrame = endFrame; + } + + void setLoopStartFrame(f_cnt_t loopStartFrame) { + m_loopStartFrame = loopStartFrame; + } + + void setLoopEndFrame(f_cnt_t loopEndFrame) { + m_loopEndFrame = loopEndFrame; + } + + void setAmplification(float amplification) { + m_amplification = amplification; + } + + const QString &getMaybeAudioFile() const + { + return m_maybeAudioFile; + } + + void setMaybeAudioFile(const QString &maybeAudioFile) + { + m_maybeAudioFile = maybeAudioFile; + } + + private: + f_cnt_t m_startFrame = 0; + f_cnt_t m_endFrame = 0; + f_cnt_t m_loopStartFrame = 0; + f_cnt_t m_loopEndFrame = 0; + float m_amplification = 1.0f; + QString m_maybeAudioFile; + }; +} + + +#endif //LMMS_SAMPLEBUFFERPLAYINFO_H diff --git a/include/lmms_basics.h b/include/lmms_basics.h index cca04e97d8f..c06b30586c2 100644 --- a/include/lmms_basics.h +++ b/include/lmms_basics.h @@ -34,6 +34,7 @@ #include #endif +#include typedef int32_t tact_t; typedef int32_t tick_t; @@ -59,7 +60,7 @@ typedef uint32_t jo_id_t; // (unique) ID of a journalling object #define likely(x) Q_LIKELY(x) #define unlikely(x) Q_UNLIKELY(x) -// windows headers define "min" and "max" macros, breaking the methods bwloe +// windows headers define "min" and "max" macros, breaking the methods below #undef min #undef max @@ -121,6 +122,9 @@ const ch_cnt_t SURROUND_CHANNELS = 2; #endif +// "In audio 1 is always the left" - umcaruje, 2017 +const ch_cnt_t RIGHT_CHANNEL_INDEX = 1; +const ch_cnt_t LEFT_CHANNEL_INDEX = 0; #ifdef LMMS_BUILD_WIN32 #define LADSPA_PATH_SEPERATOR ';' @@ -128,19 +132,29 @@ const ch_cnt_t SURROUND_CHANNELS = #define LADSPA_PATH_SEPERATOR ':' #endif +typedef std::array sampleFrame; - -typedef sample_t sampleFrame[DEFAULT_CHANNELS]; -typedef sample_t surroundSampleFrame[SURROUND_CHANNELS]; +typedef std::array surroundSampleFrame; #define ALIGN_SIZE 16 #if __GNUC__ -typedef sample_t sampleFrameA[DEFAULT_CHANNELS] __attribute__((__aligned__(ALIGN_SIZE))); +typedef std::array sampleFrameA __attribute__((__aligned__(ALIGN_SIZE))); #endif +static_assert (sizeof(sampleFrame) == sizeof(sample_t) * DEFAULT_CHANNELS, + "sampleFrame's size is not equal to the sum of its parts"); +static_assert (sizeof(surroundSampleFrame) == sizeof(sample_t) * SURROUND_CHANNELS, + "surroundSampleFrame's size is not equal to the sum of its parts"); + #define STRINGIFY(s) STR(s) #define STR(PN) #PN +enum LoopMode { + LoopOff = 0, + LoopOn, + LoopPingPong +}; + // Abstract away GUI CTRL key (linux/windows) vs ⌘ (apple) #ifdef LMMS_BUILD_APPLE # define UI_CTRL_KEY "⌘" diff --git a/plugins/audio_file_processor/audio_file_processor.cpp b/plugins/audio_file_processor/audio_file_processor.cpp index c1cd725c10e..851653cc1af 100644 --- a/plugins/audio_file_processor/audio_file_processor.cpp +++ b/plugins/audio_file_processor/audio_file_processor.cpp @@ -1,4 +1,4 @@ -/* +/* * audio_file_processor.cpp - instrument for using audio-files * * Copyright (c) 2004-2014 Tobias Doerffel @@ -74,6 +74,7 @@ Plugin::Descriptor PLUGIN_EXPORT audiofileprocessor_plugin_descriptor = audioFileProcessor::audioFileProcessor( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &audiofileprocessor_plugin_descriptor ), m_sampleBuffer(), + m_sampleBufferInfo{m_sampleBuffer.createUpdatingValue(this)}, m_ampModel( 100, 0, 500, 1, this, tr( "Amplify" ) ), m_startPointModel( 0, 0, 1, 0.0000001f, this, tr( "Start of sample" ) ), m_endPointModel( 1, 0, 1, 0.0000001f, this, tr( "End of sample" ) ), @@ -128,18 +129,18 @@ void audioFileProcessor::playNote( NotePlayHandle * _n, // played. if( m_stutterModel.value() == true && _n->frequency() < 20.0 ) { - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sampleBufferInfo->startFrame; m_nextPlayBackwards = false; return; } if( !_n->m_pluginData ) { - if( m_stutterModel.value() == true && m_nextPlayStartPoint >= m_sampleBuffer.endFrame() ) + if( m_stutterModel.value() == true && m_nextPlayStartPoint >= m_sampleBufferInfo->endFrame ) { // Restart playing the note if in stutter mode, not in loop mode, // and we're at the end of the sample. - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sampleBufferInfo->startFrame; m_nextPlayBackwards = false; } // set interpolation mode for libsamplerate @@ -210,13 +211,7 @@ void audioFileProcessor::deleteNotePluginData( NotePlayHandle * _n ) void audioFileProcessor::saveSettings( QDomDocument & _doc, QDomElement & _this ) { - _this.setAttribute( "src", m_sampleBuffer.audioFile() ); - if( m_sampleBuffer.audioFile() == "" ) - { - QString s; - _this.setAttribute( "sampledata", - m_sampleBuffer.toBase64( s ) ); - } + m_sampleBuffer.saveSettings (_doc, _this); m_reverseModel.saveSettings( _doc, _this, "reversed" ); m_loopModel.saveSettings( _doc, _this, "looped" ); m_ampModel.saveSettings( _doc, _this, "amp" ); @@ -233,21 +228,27 @@ void audioFileProcessor::saveSettings( QDomDocument & _doc, void audioFileProcessor::loadSettings( const QDomElement & _this ) { + m_isLoadingSettings = true; + if( _this.attribute( "src" ) != "" ) { setAudioFile( _this.attribute( "src" ), false ); - QString absolutePath = m_sampleBuffer.tryToMakeAbsolute( m_sampleBuffer.audioFile() ); + QString absolutePath = m_sampleBuffer.tryToMakeAbsolute( m_sampleBufferInfo->audioFile ); if ( !QFileInfo( absolutePath ).exists() ) { - QString message = tr( "Sample not found: %1" ).arg( m_sampleBuffer.audioFile() ); + QString message = tr( "Sample not found: %1" ).arg( m_sampleBufferInfo->audioFile ); Engine::getSong()->collectError( message ); } } else if( _this.attribute( "sampledata" ) != "" ) { - m_sampleBuffer.loadFromBase64( _this.attribute( "srcdata" ) ); + m_sampleBuffer = SampleBuffer( _this.attribute( "sampledata" ) , + Engine::mixer ()->baseSampleRate ()); + qWarning("Using default sampleRate. That could lead to invalid values"); + } else { + m_sampleBuffer.loadSettings (_this); } m_loopModel.loadSettings( _this, "looped" ); @@ -267,6 +268,8 @@ void audioFileProcessor::loadSettings( const QDomElement & _this ) } m_reverseModel.loadSettings( _this, "reversed" ); + // The current state of m_sampleBuffer. + m_isCurrentlyReversed = m_reverseModel.value (); m_stutterModel.loadSettings( _this, "stutter" ); if( _this.hasAttribute( "interp" ) ) @@ -279,6 +282,8 @@ void audioFileProcessor::loadSettings( const QDomElement & _this ) } pointChanged(); + + m_isLoadingSettings = false; } @@ -305,7 +310,7 @@ int audioFileProcessor::getBeatLen( NotePlayHandle * _n ) const const float freq_factor = BaseFreq / _n->frequency() * Engine::mixer()->processingSampleRate() / Engine::mixer()->baseSampleRate(); - return static_cast( floorf( ( m_sampleBuffer.endFrame() - m_sampleBuffer.startFrame() ) * freq_factor ) ); + return static_cast( floorf( ( m_sampleBufferInfo->endFrame - m_sampleBufferInfo->startFrame ) * freq_factor ) ); } @@ -326,8 +331,8 @@ void audioFileProcessor::setAudioFile( const QString & _audio_file, // is current channel-name equal to previous-filename?? if( _rename && ( instrumentTrack()->name() == - QFileInfo( m_sampleBuffer.audioFile() ).fileName() || - m_sampleBuffer.audioFile().isEmpty() ) ) + QFileInfo( m_sampleBufferInfo->audioFile ).fileName() || + m_sampleBufferInfo->audioFile.isEmpty() ) ) { // then set it to new one instrumentTrack()->setName( QFileInfo( _audio_file).fileName() ); @@ -343,8 +348,14 @@ void audioFileProcessor::setAudioFile( const QString & _audio_file, void audioFileProcessor::reverseModelChanged( void ) { - m_sampleBuffer.setReversed( m_reverseModel.value() ); - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + // Don't consider loading as a value change. + if (!m_isLoadingSettings && m_reverseModel.value () != m_isCurrentlyReversed) { + m_isCurrentlyReversed = m_reverseModel.value (); + + m_sampleBuffer.reverse (); + } + + m_nextPlayStartPoint = m_sampleBufferInfo->startFrame; m_nextPlayBackwards = false; } @@ -359,7 +370,7 @@ void audioFileProcessor::ampModelChanged( void ) void audioFileProcessor::stutterModelChanged() { - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sampleBufferInfo->startFrame; m_nextPlayBackwards = false; } @@ -428,9 +439,9 @@ void audioFileProcessor::loopPointChanged( void ) void audioFileProcessor::pointChanged( void ) { - const f_cnt_t f_start = static_cast( m_startPointModel.value() * ( m_sampleBuffer.frames()-1 ) ); - const f_cnt_t f_end = static_cast( m_endPointModel.value() * ( m_sampleBuffer.frames()-1 ) ); - const f_cnt_t f_loop = static_cast( m_loopPointModel.value() * ( m_sampleBuffer.frames()-1 ) ); + const f_cnt_t f_start = static_cast( m_startPointModel.value() * ( m_sampleBufferInfo->frames-1 ) ); + const f_cnt_t f_end = static_cast( m_endPointModel.value() * ( m_sampleBufferInfo->frames-1 ) ); + const f_cnt_t f_loop = static_cast( m_loopPointModel.value() * ( m_sampleBufferInfo->frames-1 ) ); m_nextPlayStartPoint = f_start; m_nextPlayBackwards = false; @@ -602,7 +613,9 @@ void AudioFileProcessorView::newWaveView() delete m_waveView; m_waveView = 0; } - m_waveView = new AudioFileProcessorWaveView( this, 245, 75, castModel()->m_sampleBuffer ); + m_waveView = new AudioFileProcessorWaveView( this, 245, 75, castModel()->m_sampleBuffer, + castModel()->m_sampleBufferInfo, + castModel()); m_waveView->move( 2, 172 ); m_waveView->setKnobs( dynamic_cast( m_startKnob ), @@ -648,7 +661,7 @@ void AudioFileProcessorView::paintEvent( QPaintEvent * ) audioFileProcessor * a = castModel(); QString file_name = ""; - int idx = a->m_sampleBuffer.audioFile().length(); + int idx = a->m_sampleBufferInfo->audioFile.length(); p.setFont( pointSize<8>( font() ) ); @@ -659,7 +672,7 @@ void AudioFileProcessorView::paintEvent( QPaintEvent * ) while( idx > 0 && fm.size( Qt::TextSingleLine, file_name + "..." ).width() < 210 ) { - file_name = a->m_sampleBuffer.audioFile()[--idx] + file_name; + file_name = a->m_sampleBufferInfo->audioFile[--idx] + file_name; } if( idx > 0 ) @@ -721,20 +734,24 @@ void AudioFileProcessorView::modelChanged( void ) void AudioFileProcessorWaveView::updateSampleRange() { - if( m_sampleBuffer.frames() > 1 ) + if( m_sampleBufferInfo->frames > 1 ) { - const f_cnt_t marging = ( m_sampleBuffer.endFrame() - m_sampleBuffer.startFrame() ) * 0.1; - m_from = qMax( 0, m_sampleBuffer.startFrame() - marging ); - m_to = qMin( m_sampleBuffer.endFrame() + marging, m_sampleBuffer.frames() ); + const f_cnt_t marging = ( m_sampleBufferInfo->endFrame - m_sampleBufferInfo->startFrame ) * 0.1; + m_from = qMax( 0, m_sampleBufferInfo->startFrame - marging ); + m_to = qMin( m_sampleBufferInfo->endFrame + marging, m_sampleBufferInfo->frames ); } } -AudioFileProcessorWaveView::AudioFileProcessorWaveView( QWidget * _parent, int _w, int _h, SampleBuffer& buf ) : +AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, SampleBuffer& buf , + SampleBuffer::InfoUpdatingValue &sampleBufferInfo, + audioFileProcessor *fileProcessor) : QWidget( _parent ), + m_audioFileProcessor{fileProcessor}, m_sampleBuffer( buf ), + m_sampleBufferInfo(sampleBufferInfo), m_graph( QPixmap( _w - 2 * s_padding, _h - 2 * s_padding ) ), m_from( 0 ), - m_to( m_sampleBuffer.frames() ), + m_to( m_sampleBufferInfo->frames ), m_last_from( 0 ), m_last_to( 0 ), m_last_amp( 0 ), @@ -882,11 +899,11 @@ void AudioFileProcessorWaveView::paintEvent( QPaintEvent * _pe ) const QRect graph_rect( s_padding, s_padding, width() - 2 * s_padding, height() - 2 * s_padding ); const f_cnt_t frames = m_to - m_from; - m_startFrameX = graph_rect.x() + ( m_sampleBuffer.startFrame() - m_from ) * + m_startFrameX = graph_rect.x() + ( m_sampleBufferInfo->startFrame - m_from ) * double( graph_rect.width() ) / frames; - m_endFrameX = graph_rect.x() + ( m_sampleBuffer.endFrame() - m_from ) * + m_endFrameX = graph_rect.x() + ( m_sampleBufferInfo->endFrame - m_from ) * double( graph_rect.width() ) / frames; - m_loopFrameX = graph_rect.x() + ( m_sampleBuffer.loopStartFrame() - m_from ) * + m_loopFrameX = graph_rect.x() + ( m_sampleBufferInfo->loopStartFrame - m_from ) * double( graph_rect.width() ) / frames; const int played_width_px = ( m_framesPlayed - m_from ) * double( graph_rect.width() ) / frames; @@ -961,7 +978,7 @@ void AudioFileProcessorWaveView::paintEvent( QPaintEvent * _pe ) p.setFont( pointSize<8>( font() ) ); QString length_text; - const int length = m_sampleBuffer.sampleLength(); + const int length = m_sampleBufferInfo->sampleLength(); if( length > 20000 ) { @@ -990,32 +1007,31 @@ void AudioFileProcessorWaveView::updateGraph() { if( m_to == 1 ) { - m_to = m_sampleBuffer.frames() * 0.7; + m_to = m_sampleBufferInfo->frames * 0.7; slideSamplePointToFrames( end, m_to * 0.7 ); } - if( m_from > m_sampleBuffer.startFrame() ) + if( m_from > m_sampleBufferInfo->startFrame ) { - m_from = m_sampleBuffer.startFrame(); + m_from = m_sampleBufferInfo->startFrame; } - if( m_to < m_sampleBuffer.endFrame() ) + if( m_to < m_sampleBufferInfo->endFrame ) { - m_to = m_sampleBuffer.endFrame(); + m_to = m_sampleBufferInfo->endFrame; } - if( m_sampleBuffer.reversed() != m_reversed ) - { + if(m_audioFileProcessor->isReversed () != m_reversed) { reverse(); } - else if( m_last_from == m_from && m_last_to == m_to && m_sampleBuffer.amplification() == m_last_amp ) + else if( m_last_from == m_from && m_last_to == m_to && m_sampleBufferInfo->amplification == m_last_amp ) { return; } m_last_from = m_from; m_last_to = m_to; - m_last_amp = m_sampleBuffer.amplification(); + m_last_amp = m_sampleBufferInfo->amplification; m_graph.fill( Qt::transparent ); QPainter p( &m_graph ); @@ -1033,9 +1049,9 @@ void AudioFileProcessorWaveView::updateGraph() void AudioFileProcessorWaveView::zoom( const bool _out ) { - const f_cnt_t start = m_sampleBuffer.startFrame(); - const f_cnt_t end = m_sampleBuffer.endFrame(); - const f_cnt_t frames = m_sampleBuffer.frames(); + const f_cnt_t start = m_sampleBufferInfo->startFrame; + const f_cnt_t end = m_sampleBufferInfo->endFrame; + const f_cnt_t frames = m_sampleBufferInfo->frames; const f_cnt_t d_from = start - m_from; const f_cnt_t d_to = m_to - end; @@ -1068,7 +1084,7 @@ void AudioFileProcessorWaveView::zoom( const bool _out ) ); } - if( double( new_to - new_from ) / m_sampleBuffer.sampleRate() > 0.05 ) + if( double( new_to - new_from ) / m_sampleBufferInfo->sampleRate > 0.05 ) { m_from = new_from; m_to = new_to; @@ -1087,8 +1103,8 @@ void AudioFileProcessorWaveView::slide( int _px ) step = -step; } - f_cnt_t step_from = qBound( 0, m_from + step, m_sampleBuffer.frames() ) - m_from; - f_cnt_t step_to = qBound( m_from + 1, m_to + step, m_sampleBuffer.frames() ) - m_to; + f_cnt_t step_from = qBound( 0, m_from + step, m_sampleBufferInfo->frames ) - m_from; + f_cnt_t step_to = qBound( m_from + 1, m_to + step, m_sampleBufferInfo->frames ) - m_to; step = qAbs( step_from ) < qAbs( step_to ) ? step_from : step_to; @@ -1149,7 +1165,7 @@ void AudioFileProcessorWaveView::slideSamplePointByFrames( knobType _point, f_cn } else { - const double v = static_cast( _frames ) / m_sampleBuffer.frames(); + const double v = static_cast( _frames ) / m_sampleBufferInfo->frames; if( _slide_to ) { a_knob->slideTo( v ); @@ -1166,11 +1182,11 @@ void AudioFileProcessorWaveView::slideSamplePointByFrames( knobType _point, f_cn void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) { - if( m_sampleBuffer.frames() <= 1 ) + if( m_sampleBufferInfo->frames <= 1 ) { return; } - const double v = static_cast( _frames ) / m_sampleBuffer.frames(); + const double v = static_cast( _frames ) / m_sampleBufferInfo->frames; if( m_startKnob ) { m_startKnob->slideBy( v, false ); } @@ -1188,14 +1204,14 @@ void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) void AudioFileProcessorWaveView::reverse() { slideSampleByFrames( - m_sampleBuffer.frames() - - m_sampleBuffer.endFrame() - - m_sampleBuffer.startFrame() + m_sampleBufferInfo->frames + - m_sampleBufferInfo->endFrame + - m_sampleBufferInfo->startFrame ); const f_cnt_t from = m_from; - m_from = m_sampleBuffer.frames() - m_to; - m_to = m_sampleBuffer.frames() - from; + m_from = m_sampleBufferInfo->frames - m_to; + m_to = m_sampleBufferInfo->frames - from; m_reversed = ! m_reversed; } @@ -1238,7 +1254,7 @@ float AudioFileProcessorWaveView::knob::getValue( const QPoint & _p ) { const double dec_fact = ! m_waveView ? 1 : double( m_waveView->m_to - m_waveView->m_from ) - / m_waveView->m_sampleBuffer.frames(); + / m_waveView->m_sampleBufferInfo->frames; const float inc = ::Knob::getValue( _p ) * dec_fact; return inc; @@ -1259,12 +1275,12 @@ bool AudioFileProcessorWaveView::knob::checkBound( double _v ) const return false; const double d1 = qAbs( m_relatedKnob->model()->value() - model()->value() ) - * ( m_waveView->m_sampleBuffer.frames() ) - / m_waveView->m_sampleBuffer.sampleRate(); + * ( m_waveView->m_sampleBufferInfo->frames ) + / m_waveView->m_sampleBufferInfo->sampleRate; const double d2 = qAbs( m_relatedKnob->model()->value() - _v ) - * ( m_waveView->m_sampleBuffer.frames() ) - / m_waveView->m_sampleBuffer.sampleRate(); + * ( m_waveView->m_sampleBufferInfo->frames ) + / m_waveView->m_sampleBufferInfo->sampleRate; return d1 < d2 || d2 > 0.005; } diff --git a/plugins/audio_file_processor/audio_file_processor.h b/plugins/audio_file_processor/audio_file_processor.h index d17be147c0c..1beccdf091a 100644 --- a/plugins/audio_file_processor/audio_file_processor.h +++ b/plugins/audio_file_processor/audio_file_processor.h @@ -66,6 +66,10 @@ class audioFileProcessor : public Instrument virtual PluginView * instantiateView( QWidget * _parent ); + bool isReversed () const + { + return m_isCurrentlyReversed; + } public slots: void setAudioFile( const QString & _audio_file, bool _rename = true ); @@ -89,6 +93,7 @@ private slots: typedef SampleBuffer::handleState handleState; SampleBuffer m_sampleBuffer; + SampleBuffer::InfoUpdatingValue m_sampleBufferInfo; FloatModel m_ampModel; FloatModel m_startPointModel; @@ -102,6 +107,20 @@ private slots: f_cnt_t m_nextPlayStartPoint; bool m_nextPlayBackwards; + /** + * @brief Is currently m_sampleBuffer reversed? + * + * Since m_sampleBuffer has no longer the `reversed` + * attribute, we track it here; m_reverseModel is + * only what the user want it to be. + */ + bool m_isCurrentlyReversed = false; + + /** + * @brief Are we in the middle of loadSettings? + */ + bool m_isLoadingSettings = false; + friend class AudioFileProcessorView; } ; @@ -154,6 +173,8 @@ protected slots: class AudioFileProcessorWaveView : public QWidget { Q_OBJECT + + protected: virtual void enterEvent( QEvent * _e ); virtual void leaveEvent( QEvent * _e ); @@ -235,7 +256,9 @@ public slots: sample_loop } ; + audioFileProcessor *m_audioFileProcessor; SampleBuffer& m_sampleBuffer; + SampleBuffer::InfoUpdatingValue &m_sampleBufferInfo; QPixmap m_graph; f_cnt_t m_from; f_cnt_t m_to; @@ -256,7 +279,8 @@ public slots: bool m_animation; public: - AudioFileProcessorWaveView( QWidget * _parent, int _w, int _h, SampleBuffer& buf ); + AudioFileProcessorWaveView(QWidget *_parent, int _w, int _h, SampleBuffer &buf, + SampleBuffer::InfoUpdatingValue &sampleBufferInfo, audioFileProcessor *fileProcessor); void setKnobs(knob *_start, knob *_end, knob *_loop ); diff --git a/plugins/patman/patman.cpp b/plugins/patman/patman.cpp index 0ea9968dae7..0f9f76a0684 100644 --- a/plugins/patman/patman.cpp +++ b/plugins/patman/patman.cpp @@ -148,10 +148,10 @@ void patmanInstrument::playNote( NotePlayHandle * _n, handle_data * hdata = (handle_data *)_n->m_pluginData; float play_freq = hdata->tuned ? _n->frequency() : - hdata->sample->frequency(); + hdata->sampleInfo->frequency; if( hdata->sample->play( _working_buffer + offset, hdata->state, frames, - play_freq, m_loopedModel.value() ? SampleBuffer::LoopOn : SampleBuffer::LoopOff ) ) + play_freq, m_loopedModel.value() ? LoopOn : LoopOff ) ) { applyRelease( _working_buffer, _n ); instrumentTrack()->processAudioBuffer( _working_buffer, @@ -169,7 +169,7 @@ void patmanInstrument::playNote( NotePlayHandle * _n, void patmanInstrument::deleteNotePluginData( NotePlayHandle * _n ) { handle_data * hdata = (handle_data *)_n->m_pluginData; - sharedObject::unref( hdata->sample ); + delete hdata->state; delete hdata; } @@ -345,7 +345,7 @@ patmanInstrument::LoadErrors patmanInstrument::loadPatch( } } - sampleFrame * data = new sampleFrame[frames]; + SampleBuffer::DataVector data(frames); for( f_cnt_t frame = 0; frame < frames; ++frame ) { @@ -356,9 +356,8 @@ patmanInstrument::LoadErrors patmanInstrument::loadPatch( } } - SampleBuffer* psample = new SampleBuffer( data, frames ); + auto psample = std::make_shared( std::move(data), sample_rate ); psample->setFrequency( root_freq / 1000.0f ); - psample->setSampleRate( sample_rate ); if( modes & MODES_LOOPING ) { @@ -366,10 +365,10 @@ patmanInstrument::LoadErrors patmanInstrument::loadPatch( psample->setLoopEndFrame( loop_end ); } - m_patchSamples.push_back( psample ); + auto info = psample->createUpdatingValue(this); + m_patchSamples.push_back( {std::move(psample), std::move(info)}); delete[] wave_samples; - delete[] data; } fclose( fd ); return( LoadOK ); @@ -380,11 +379,7 @@ patmanInstrument::LoadErrors patmanInstrument::loadPatch( void patmanInstrument::unloadCurrentPatch( void ) { - while( !m_patchSamples.empty() ) - { - sharedObject::unref( m_patchSamples.back() ); - m_patchSamples.pop_back(); - } + m_patchSamples.clear(); } @@ -395,31 +390,28 @@ void patmanInstrument::selectSample( NotePlayHandle * _n ) const float freq = _n->frequency(); float min_dist = HUGE_VALF; - SampleBuffer* sample = NULL; + std::shared_ptr sample; - for( QVector::iterator it = m_patchSamples.begin(); it != m_patchSamples.end(); ++it ) + for( auto &element : m_patchSamples ) { - float patch_freq = ( *it )->frequency(); + float patch_freq = element.second->frequency; float dist = freq >= patch_freq ? freq / patch_freq : patch_freq / freq; if( dist < min_dist ) { min_dist = dist; - sample = *it; + sample = element.first; } } - handle_data * hdata = new handle_data; - hdata->tuned = m_tunedModel.value(); - if( sample ) + if( !sample ) { - hdata->sample = sharedObject::ref( sample ); - } - else - { - hdata->sample = new SampleBuffer( NULL, 0 ); + sample = std::make_shared(); } + + handle_data * hdata = new handle_data(std::move(sample), this); + hdata->tuned = m_tunedModel.value(); hdata->state = new SampleBuffer::handleState( _n->hasDetuningInfo() ); _n->m_pluginData = hdata; diff --git a/plugins/patman/patman.h b/plugins/patman/patman.h index a3b5a39b898..a614fb8f8f1 100644 --- a/plugins/patman/patman.h +++ b/plugins/patman/patman.h @@ -31,6 +31,7 @@ #include "SampleBuffer.h" #include "AutomatableModel.h" #include "MemoryManager.h" +#include class PixmapButton; @@ -77,16 +78,23 @@ public slots: private: - typedef struct - { + struct handle_data { MM_OPERATORS + + handle_data(std::shared_ptr &&buffer, QObject *parent) + : sample{std::move(buffer)}, + sampleInfo(sample->createUpdatingValue(parent)) + { + } + SampleBuffer::handleState* state; bool tuned; - SampleBuffer* sample; - } handle_data; + std::shared_ptr sample; + SampleBuffer::InfoUpdatingValue sampleInfo; + }; QString m_patchFile; - QVector m_patchSamples; + std::vector, SampleBuffer::InfoUpdatingValue>> m_patchSamples; BoolModel m_loopedModel; BoolModel m_tunedModel; diff --git a/plugins/triple_oscillator/TripleOscillator.cpp b/plugins/triple_oscillator/TripleOscillator.cpp index a883f75fa32..49712ab91e0 100644 --- a/plugins/triple_oscillator/TripleOscillator.cpp +++ b/plugins/triple_oscillator/TripleOscillator.cpp @@ -89,6 +89,7 @@ OscillatorObject::OscillatorObject( Model * _parent, int _idx ) : tr( "Modulation type %1" ).arg( _idx+1 ) ), m_sampleBuffer( new SampleBuffer ), + m_sampleBufferInfo(m_sampleBuffer->createUpdatingValue(this)), m_volumeLeft( 0.0f ), m_volumeRight( 0.0f ), m_detuningLeft( 0.0f ), @@ -130,7 +131,7 @@ OscillatorObject::OscillatorObject( Model * _parent, int _idx ) : OscillatorObject::~OscillatorObject() { - sharedObject::unref( m_sampleBuffer ); + delete m_sampleBuffer; } @@ -254,7 +255,7 @@ void TripleOscillator::saveSettings( QDomDocument & _doc, QDomElement & _this ) m_osc[i]->m_modulationAlgoModel.saveSettings( _doc, _this, "modalgo" + QString::number( i+1 ) ); _this.setAttribute( "userwavefile" + is, - m_osc[i]->m_sampleBuffer->audioFile() ); + m_osc[i]->m_sampleBufferInfo->audioFile ); } } @@ -280,7 +281,7 @@ void TripleOscillator::loadSettings( const QDomElement & _this ) m_osc[i]->m_modulationAlgoModel.loadSettings( _this, "modalgo" + QString::number( i+1 ) ); m_osc[i]->m_sampleBuffer->setAudioFile( _this.attribute( - "userwavefile" + is ) ); + "userwavefile" + is ), true ); } } diff --git a/plugins/triple_oscillator/TripleOscillator.h b/plugins/triple_oscillator/TripleOscillator.h index fb2b48a410b..773be8b4439 100644 --- a/plugins/triple_oscillator/TripleOscillator.h +++ b/plugins/triple_oscillator/TripleOscillator.h @@ -61,6 +61,7 @@ class OscillatorObject : public Model IntModel m_waveShapeModel; IntModel m_modulationAlgoModel; SampleBuffer* m_sampleBuffer; + SampleBuffer::InfoUpdatingValue m_sampleBufferInfo; float m_volumeLeft; float m_volumeRight; diff --git a/src/3rdparty/weakjack/CMakeLists.txt b/src/3rdparty/weakjack/CMakeLists.txt index 7600c3915ed..c0146f12fc2 100644 --- a/src/3rdparty/weakjack/CMakeLists.txt +++ b/src/3rdparty/weakjack/CMakeLists.txt @@ -1,5 +1,5 @@ # Use weak jack library linking -IF(LMMS_HAVE_WEAKJACK) +IF(LMMS_HAVE_WEAKJACK AND LMMS_BUILD_LINUX) SET(CMAKE_C_FLAGS "-std=c11") # Enable weakjack, disable metadata support diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4ac6bf1332f..a434ee37ec3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -229,7 +229,7 @@ IF(NOT MSVC) "${MINGW_PREFIX}/bin/libfftw3f-3.dll" "${MINGW_PREFIX}/bin/libFLAC-8.dll" "${MINGW_PREFIX}/bin/libpng16-16.dll" - "${MINGW_PREFIX}/bin/SDL.dll" + "${MINGW_PREFIX}/bin/SDL2.dll" "${MINGW_PREFIX}/bin/libglib-2.0-0.dll" "${MINGW_PREFIX}/bin/libgthread-2.0-0.dll" "${MINGW_PREFIX}/bin/zlib1.dll" @@ -393,3 +393,4 @@ ELSE(NOT MSVC) # DESTINATION .) #ENDIF() ENDIF(NOT MSVC) + diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ba41e089c7a..fa441dfc396 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -85,7 +85,6 @@ set(LMMS_SRCS core/audio/AudioPortAudio.cpp core/audio/AudioSoundIo.cpp core/audio/AudioPulseAudio.cpp - core/audio/AudioSampleRecorder.cpp core/audio/AudioSdl.cpp core/midi/MidiAlsaRaw.cpp @@ -100,5 +99,13 @@ set(LMMS_SRCS core/midi/MidiTime.cpp core/midi/MidiWinMM.cpp + core/SampleBufferVisualizer.cpp + + core/samplebuffer/SampleBufferData.cpp + core/samplebuffer/SampleBufferFileHelper.cpp + + core/MemoryUtils.cpp + + PARENT_SCOPE ) diff --git a/src/core/Effect.cpp b/src/core/Effect.cpp index c842977532f..a85afd9b5c8 100644 --- a/src/core/Effect.cpp +++ b/src/core/Effect.cpp @@ -203,8 +203,8 @@ void Effect::resample( int _i, const sampleFrame * _src_buf, } m_srcData[_i].input_frames = _frames; m_srcData[_i].output_frames = Engine::mixer()->framesPerPeriod(); - m_srcData[_i].data_in = (float *) _src_buf[0]; - m_srcData[_i].data_out = _dst_buf[0]; + m_srcData[_i].data_in = (float *) _src_buf[0].data (); + m_srcData[_i].data_out = _dst_buf[0].data (); m_srcData[_i].src_ratio = (double) _dst_sr / _src_sr; m_srcData[_i].end_of_input = 0; int error; diff --git a/src/core/Engine.cpp b/src/core/Engine.cpp index 50e25b0b416..94469c9972b 100644 --- a/src/core/Engine.cpp +++ b/src/core/Engine.cpp @@ -92,8 +92,7 @@ void LmmsCore::destroy() deleteHelper( &s_bbTrackContainer ); deleteHelper( &s_dummyTC ); - deleteHelper( &s_fxMixer ); - deleteHelper( &s_mixer ); + deleteHelper(&s_fxMixer); deleteHelper( &s_ladspaManager ); @@ -103,6 +102,13 @@ void LmmsCore::destroy() deleteHelper( &s_song ); delete ConfigManager::inst(); + + deleteHelper(&s_mixer); +} + +float LmmsCore::framesPerTick(sample_rate_t sample_rate) { + return sample_rate * 60.0f * 4 / + DefaultTicksPerTact / s_song->getTempo(); } diff --git a/src/core/EnvelopeAndLfoParameters.cpp b/src/core/EnvelopeAndLfoParameters.cpp index 27a73b2c1a5..70159ea093a 100644 --- a/src/core/EnvelopeAndLfoParameters.cpp +++ b/src/core/EnvelopeAndLfoParameters.cpp @@ -118,7 +118,8 @@ EnvelopeAndLfoParameters::EnvelopeAndLfoParameters( m_controlEnvAmountModel( false, this, tr( "Modulate env amount" ) ), m_lfoFrame( 0 ), m_lfoAmountIsZero( false ), - m_lfoShapeData( NULL ) + m_lfoShapeData( NULL ), + m_userWaveInfo(m_userWave.createUpdatingValue(this)) { m_amountModel.setCenterValue( 0 ); m_lfoAmountModel.setCenterValue( 0 ); @@ -354,7 +355,7 @@ void EnvelopeAndLfoParameters::saveSettings( QDomDocument & _doc, m_lfoAmountModel.saveSettings( _doc, _parent, "lamt" ); m_x100Model.saveSettings( _doc, _parent, "x100" ); m_controlEnvAmountModel.saveSettings( _doc, _parent, "ctlenvamt" ); - _parent.setAttribute( "userwavefile", m_userWave.audioFile() ); + _parent.setAttribute( "userwavefile", m_userWaveInfo->audioFile ); } diff --git a/src/core/LfoController.cpp b/src/core/LfoController.cpp index 2b2db2f1471..786404ce0c6 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -45,7 +45,8 @@ LfoController::LfoController( Model * _parent ) : m_phaseOffset( 0 ), m_currentPhase( 0 ), m_sampleFunction( &Oscillator::sinSample ), - m_userDefSampleBuffer( new SampleBuffer ) + m_userDefSampleBuffer( new SampleBuffer ), + m_userDefSampleBufferInfo{m_userDefSampleBuffer->createUpdatingValue(this)} { setSampleExact( true ); connect( &m_waveModel, SIGNAL( dataChanged() ), @@ -71,7 +72,7 @@ LfoController::LfoController( Model * _parent ) : LfoController::~LfoController() { - sharedObject::unref( m_userDefSampleBuffer ); + delete m_userDefSampleBuffer; m_baseModel.disconnect( this ); m_speedModel.disconnect( this ); m_amountModel.disconnect( this ); @@ -192,7 +193,7 @@ void LfoController::saveSettings( QDomDocument & _doc, QDomElement & _this ) m_phaseModel.saveSettings( _doc, _this, "phase" ); m_waveModel.saveSettings( _doc, _this, "wave" ); m_multiplierModel.saveSettings( _doc, _this, "multiplier" ); - _this.setAttribute( "userwavefile" , m_userDefSampleBuffer->audioFile() ); + _this.setAttribute( "userwavefile" , m_userDefSampleBufferInfo->audioFile); } diff --git a/src/core/MemoryUtils.cpp b/src/core/MemoryUtils.cpp new file mode 100644 index 00000000000..e2d78cfc850 --- /dev/null +++ b/src/core/MemoryUtils.cpp @@ -0,0 +1,9 @@ +#include "MemoryUtils.h" + +namespace internal { + void qobjectDeleter(QObject *object) + { + object->deleteLater(); + } + +} diff --git a/src/core/Mixer.cpp b/src/core/Mixer.cpp index 49cda8c34e2..65ff17ec2ab 100644 --- a/src/core/Mixer.cpp +++ b/src/core/Mixer.cpp @@ -309,8 +309,7 @@ bool Mixer::criticalXRuns() const -void Mixer::pushInputFrames( sampleFrame * _ab, const f_cnt_t _frames ) -{ +void Mixer::pushInputFrames(const sampleFrame * _ab, const f_cnt_t _frames) { requestChangeInModel(); f_cnt_t frames = m_inputBufferFrames[ m_inputBufferWrite ]; @@ -331,6 +330,7 @@ void Mixer::pushInputFrames( sampleFrame * _ab, const f_cnt_t _frames ) } memcpy( &buf[ frames ], _ab, _frames * sizeof( sampleFrame ) ); + m_inputBufferFrames[ m_inputBufferWrite ] += _frames; doneChangeInModel(); @@ -397,7 +397,8 @@ const surroundSampleFrame * Mixer::renderNextBuffer() if( it != m_playHandles.end() ) { - ( *it )->audioPort()->removePlayHandle( ( *it ) ); + if ((*it )->audioPort()) + ( *it )->audioPort()->removePlayHandle( ( *it ) ); if( ( *it )->type() == PlayHandle::TypeNotePlayHandle ) { NotePlayHandleManager::release( (NotePlayHandle*) *it ); @@ -451,7 +452,8 @@ const surroundSampleFrame * Mixer::renderNextBuffer() } if( ( *it )->isFinished() ) { - ( *it )->audioPort()->removePlayHandle( ( *it ) ); + if (( *it )->audioPort()) + ( *it )->audioPort()->removePlayHandle( ( *it ) ); if( ( *it )->type() == PlayHandle::TypeNotePlayHandle ) { NotePlayHandleManager::release( (NotePlayHandle*) *it ); @@ -576,7 +578,6 @@ void Mixer::changeQuality( const struct qualitySettings & _qs ) - void Mixer::setAudioDevice( AudioDevice * _dev, bool startNow ) { @@ -679,7 +680,8 @@ bool Mixer::addPlayHandle( PlayHandle* handle ) if( criticalXRuns() == false ) { m_newPlayHandles.push( handle ); - handle->audioPort()->addPlayHandle( handle ); + if (handle->audioPort()) + handle->audioPort()->addPlayHandle( handle ); return true; } @@ -701,7 +703,8 @@ void Mixer::removePlayHandle( PlayHandle * _ph ) if( _ph->affinityMatters() && _ph->affinity() == QThread::currentThread() ) { - _ph->audioPort()->removePlayHandle( _ph ); + if (_ph->audioPort()) + _ph->audioPort()->removePlayHandle( _ph ); bool removedFromList = false; // Check m_newPlayHandles first because doing it the other way around // creates a race condition @@ -761,7 +764,8 @@ void Mixer::removePlayHandlesOfTypes( Track * _track, const quint8 types ) { if( ( *it )->isFromTrack( _track ) && ( ( *it )->type() & types ) ) { - ( *it )->audioPort()->removePlayHandle( ( *it ) ); + if (( *it )->audioPort()) + ( *it )->audioPort()->removePlayHandle( ( *it ) ); if( ( *it )->type() == PlayHandle::TypeNotePlayHandle ) { NotePlayHandleManager::release( (NotePlayHandle*) *it ); diff --git a/src/core/PlayHandle.cpp b/src/core/PlayHandle.cpp index 9e92019a6b6..f62e25e6bb4 100644 --- a/src/core/PlayHandle.cpp +++ b/src/core/PlayHandle.cpp @@ -38,7 +38,8 @@ PlayHandle::PlayHandle(const Type type, f_cnt_t offset) : m_affinity(QThread::currentThread()), m_playHandleBuffer(BufferManager::acquire()), m_bufferReleased(true), - m_usesBuffer(true) + m_usesBuffer(true), + m_audioPort{nullptr} { } diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index 0f58e756007..381eaffeb24 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -26,621 +26,108 @@ #include -#include #include #include #include - - -#include - -#define OV_EXCLUDE_STATIC_CALLBACKS -#ifdef LMMS_HAVE_OGGVORBIS -#include -#endif - -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H -#include -#endif - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H -#include -#endif - - +#include +#include #include "base64.h" #include "ConfigManager.h" #include "DrumSynth.h" -#include "endian_handling.h" -#include "Engine.h" -#include "GuiApplication.h" -#include "Mixer.h" #include "FileDialog.h" +#include "internal/SampleBufferFileHelper.h" +#include "lmms_math.h" +#include "interpolation.h" - -SampleBuffer::SampleBuffer() : - m_audioFile( "" ), - m_origData( NULL ), - m_origFrames( 0 ), - m_data( NULL ), - m_frames( 0 ), - m_startFrame( 0 ), - m_endFrame( 0 ), - m_loopStartFrame( 0 ), - m_loopEndFrame( 0 ), - m_amplification( 1.0f ), - m_reversed( false ), - m_frequency( BaseFreq ), - m_sampleRate( Engine::mixer()->baseSampleRate() ) +SampleBuffer::SampleBuffer(const QString &_audio_file, bool ignoreError) + : SampleBuffer{internal::SampleBufferFileHelper::Load(_audio_file, ignoreError)} { - - connect( Engine::mixer(), SIGNAL( sampleRateChanged() ), this, SLOT( sampleRateChanged() ) ); - update(); + m_playInfo->setMaybeAudioFile(_audio_file); } - -SampleBuffer::SampleBuffer( const QString & _audio_file, - bool _is_base64_data ) - : SampleBuffer() -{ - if( _is_base64_data ) - { - loadFromBase64( _audio_file ); - } - else - { - m_audioFile = _audio_file; - update(); - } -} - - - - -SampleBuffer::SampleBuffer( const sampleFrame * _data, const f_cnt_t _frames ) - : SampleBuffer() -{ - if( _frames > 0 ) - { - m_origData = MM_ALLOC( sampleFrame, _frames ); - memcpy( m_origData, _data, _frames * BYTES_PER_FRAME ); - m_origFrames = _frames; - update(); - } -} - - - - -SampleBuffer::SampleBuffer( const f_cnt_t _frames ) - : SampleBuffer() +SampleBuffer::SampleBuffer(SampleBuffer::DataVector &&movedData, sample_rate_t sampleRate) + : SampleBuffer{internal::SampleBufferData{std::move(movedData), sampleRate}} { - if( _frames > 0 ) - { - m_origData = MM_ALLOC( sampleFrame, _frames ); - memset( m_origData, 0, _frames * BYTES_PER_FRAME ); - m_origFrames = _frames; - update(); - } } - - - -SampleBuffer::~SampleBuffer() +SampleBuffer::SampleBuffer(SampleBuffer &&sampleBuffer) noexcept + : + m_data{std::move(sampleBuffer.m_data)}, + m_playInfo(std::move(sampleBuffer.m_playInfo)) { - MM_FREE( m_origData ); - MM_FREE( m_data ); + emit sampleUpdated(UpdateType::Clear); } +SampleBuffer &SampleBuffer::operator=(SampleBuffer &&other) noexcept{ + DataChangeHelper helper(this, UpdateType::Clear); + m_data = std::move(other.m_data); + m_playInfo = std::move(other.m_playInfo); -void SampleBuffer::sampleRateChanged() -{ - update( true ); + return *this; } - -void SampleBuffer::update( bool _keep_settings ) -{ - const bool lock = ( m_data != NULL ); - if( lock ) - { - Engine::mixer()->requestChangeInModel(); - m_varLock.lockForWrite(); - MM_FREE( m_data ); - } - - // File size and sample length limits - const int fileSizeMax = 300; // MB - const int sampleLengthMax = 90; // Minutes - - bool fileLoadError = false; - if( m_audioFile.isEmpty() && m_origData != NULL && m_origFrames > 0 ) - { - // TODO: reverse- and amplification-property is not covered - // by following code... - m_data = MM_ALLOC( sampleFrame, m_origFrames ); - memcpy( m_data, m_origData, m_origFrames * BYTES_PER_FRAME ); - if( _keep_settings == false ) - { - m_frames = m_origFrames; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = m_frames; - } - } - else if( !m_audioFile.isEmpty() ) - { - QString file = tryToMakeAbsolute( m_audioFile ); - int_sample_t * buf = NULL; - sample_t * fbuf = NULL; - ch_cnt_t channels = DEFAULT_CHANNELS; - sample_rate_t samplerate = Engine::mixer()->baseSampleRate(); - m_frames = 0; - - const QFileInfo fileInfo( file ); - if( fileInfo.size() > fileSizeMax * 1024 * 1024 ) - { - fileLoadError = true; - } - else - { - // Use QFile to handle unicode file names on Windows - QFile f(file); - f.open(QIODevice::ReadOnly); - SNDFILE * snd_file; - SF_INFO sf_info; - sf_info.format = 0; - if( ( snd_file = sf_open_fd( f.handle(), SFM_READ, &sf_info, false ) ) != NULL ) - { - f_cnt_t frames = sf_info.frames; - int rate = sf_info.samplerate; - if( frames / rate > sampleLengthMax * 60 ) - { - fileLoadError = true; - } - sf_close( snd_file ); - } - f.close(); - } - - if( !fileLoadError ) - { -#ifdef LMMS_HAVE_OGGVORBIS - // workaround for a bug in libsndfile or our libsndfile decoder - // causing some OGG files to be distorted -> try with OGG Vorbis - // decoder first if filename extension matches "ogg" - if( m_frames == 0 && fileInfo.suffix() == "ogg" ) - { - m_frames = decodeSampleOGGVorbis( file, buf, channels, samplerate ); - } -#endif - if( m_frames == 0 ) - { - m_frames = decodeSampleSF( file, fbuf, channels, - samplerate ); - } -#ifdef LMMS_HAVE_OGGVORBIS - if( m_frames == 0 ) - { - m_frames = decodeSampleOGGVorbis( file, buf, channels, - samplerate ); - } -#endif - if( m_frames == 0 ) - { - m_frames = decodeSampleDS( file, buf, channels, - samplerate ); - } - } - - if ( m_frames == 0 || fileLoadError ) // if still no frames, bail - { - // sample couldn't be decoded, create buffer containing - // one sample-frame - m_data = MM_ALLOC( sampleFrame, 1 ); - memset( m_data, 0, sizeof( *m_data ) ); - m_frames = 1; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = 1; - } - else // otherwise normalize sample rate - { - normalizeSampleRate( samplerate, _keep_settings ); - } - } - else +void SampleBuffer::saveSettings(QDomDocument &doc, QDomElement &_this) { { - // neither an audio-file nor a buffer to copy from, so create - // buffer containing one sample-frame - m_data = MM_ALLOC( sampleFrame, 1 ); - memset( m_data, 0, sizeof( *m_data ) ); - m_frames = 1; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = 1; + QString string; + _this.setAttribute("data", toBase64(string)); } - if( lock ) - { - m_varLock.unlock(); - Engine::mixer()->doneChangeInModel(); - } - - emit sampleUpdated(); - - if( fileLoadError ) - { - QString title = tr( "Fail to open file" ); - QString message = tr( "Audio files are limited to %1 MB " - "in size and %2 minutes of playing time" - ).arg( fileSizeMax ).arg( sampleLengthMax ); - if( gui ) - { - QMessageBox::information( NULL, - title, message, QMessageBox::Ok ); - } - else - { - fprintf( stderr, "%s\n", message.toUtf8().constData() ); - } - } + auto data = guardedData(); + _this.setAttribute("sampleRate", data->getSampleRate()); + _this.setAttribute("amplification", data->getAmplification()); + _this.setAttribute("frequency", data->getFrequency()); } +void SampleBuffer::loadSettings(const QDomElement &_this) { + sample_rate_t loadingSampleRate = Engine::mixer()->baseSampleRate(); -void SampleBuffer::convertIntToFloat ( int_sample_t * & _ibuf, f_cnt_t _frames, int _channels) -{ - // following code transforms int-samples into - // float-samples and does amplifying & reversing - const float fac = 1 / OUTPUT_SAMPLE_MULTIPLIER; - m_data = MM_ALLOC( sampleFrame, _frames ); - const int ch = ( _channels > 1 ) ? 1 : 0; - - // if reversing is on, we also reverse when - // scaling - if( m_reversed ) - { - int idx = ( _frames - 1 ) * _channels; - for( f_cnt_t frame = 0; frame < _frames; - ++frame ) - { - m_data[frame][0] = _ibuf[idx+0] * fac; - m_data[frame][1] = _ibuf[idx+ch] * fac; - idx -= _channels; - } - } - else - { - int idx = 0; - for( f_cnt_t frame = 0; frame < _frames; - ++frame ) - { - m_data[frame][0] = _ibuf[idx+0] * fac; - m_data[frame][1] = _ibuf[idx+ch] * fac; - idx += _channels; - } + if (_this.hasAttribute("sampleRate")) { + loadingSampleRate = _this.attribute("sampleRate").toUInt(); + } else { + qWarning("SampleBuffer::loadSettings: Using default sampleRate. That could lead to invalid values"); } - delete[] _ibuf; -} - -void SampleBuffer::directFloatWrite ( sample_t * & _fbuf, f_cnt_t _frames, int _channels) - -{ - - m_data = MM_ALLOC( sampleFrame, _frames ); - const int ch = ( _channels > 1 ) ? 1 : 0; - - // if reversing is on, we also reverse when - // scaling - if( m_reversed ) - { - int idx = ( _frames - 1 ) * _channels; - for( f_cnt_t frame = 0; frame < _frames; - ++frame ) - { - m_data[frame][0] = _fbuf[idx+0]; - m_data[frame][1] = _fbuf[idx+ch]; - idx -= _channels; - } - } - else - { - int idx = 0; - for( f_cnt_t frame = 0; frame < _frames; - ++frame ) - { - m_data[frame][0] = _fbuf[idx+0]; - m_data[frame][1] = _fbuf[idx+ch]; - idx += _channels; - } + if (_this.hasAttribute("data")) { + *m_data = internal::SampleBufferData::loadFromBase64(_this.attribute("data"), loadingSampleRate); } - delete[] _fbuf; -} - - -void SampleBuffer::normalizeSampleRate( const sample_rate_t _src_sr, - bool _keep_settings ) -{ - // do samplerate-conversion to our default-samplerate - if( _src_sr != Engine::mixer()->baseSampleRate() ) - { - SampleBuffer * resampled = resample( _src_sr, - Engine::mixer()->baseSampleRate() ); - MM_FREE( m_data ); - m_frames = resampled->frames(); - m_data = MM_ALLOC( sampleFrame, m_frames ); - memcpy( m_data, resampled->data(), m_frames * - sizeof( sampleFrame ) ); - delete resampled; + if (_this.hasAttribute("amplification")) { + m_data->setAmplification(_this.attribute("amplification").toFloat()); } - if( _keep_settings == false ) - { - // update frame-variables - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = m_frames; + if (_this.hasAttribute("frequency")) { + m_data->setFrequency(_this.attribute("frequency").toFloat()); } } +bool SampleBuffer::play(sampleFrame *_ab, handleState *_state, + const fpp_t _frames, + const float _freq, + const LoopMode _loopmode) { + f_cnt_t startFrame = m_playInfo->getStartFrame(); + f_cnt_t endFrame = m_playInfo->getEndFrame(); + f_cnt_t loopStartFrame = m_playInfo->getLoopStartFrame(); + f_cnt_t loopEndFrame = m_playInfo->getLoopEndFrame(); - - -f_cnt_t SampleBuffer::decodeSampleSF(QString _f, - sample_t * & _buf, - ch_cnt_t & _channels, - sample_rate_t & _samplerate ) -{ - SNDFILE * snd_file; - SF_INFO sf_info; - sf_info.format = 0; - f_cnt_t frames = 0; - bool sf_rr = false; - - - // Use QFile to handle unicode file names on Windows - QFile f(_f); - f.open(QIODevice::ReadOnly); - if( ( snd_file = sf_open_fd( f.handle(), SFM_READ, &sf_info, false ) ) != NULL ) - { - frames = sf_info.frames; - - _buf = new sample_t[sf_info.channels * frames]; - sf_rr = sf_read_float( snd_file, _buf, sf_info.channels * frames ); - - if( sf_rr < sf_info.channels * frames ) - { -#ifdef DEBUG_LMMS - qDebug( "SampleBuffer::decodeSampleSF(): could not read" - " sample %s: %s", _f, sf_strerror( NULL ) ); -#endif - } - _channels = sf_info.channels; - _samplerate = sf_info.samplerate; - - sf_close( snd_file ); - } - else - { -#ifdef DEBUG_LMMS - qDebug( "SampleBuffer::decodeSampleSF(): could not load " - "sample %s: %s", _f, sf_strerror( NULL ) ); -#endif - } - f.close(); - - //write down either directly or convert i->f depending on file type - - if ( frames > 0 && _buf != NULL ) - { - directFloatWrite ( _buf, frames, _channels); - } - - return frames; -} - - - - -#ifdef LMMS_HAVE_OGGVORBIS - -// callback-functions for reading ogg-file - -size_t qfileReadCallback( void * _ptr, size_t _size, size_t _n, void * _udata ) -{ - return static_cast( _udata )->read( (char*) _ptr, - _size * _n ); -} - - - - -int qfileSeekCallback( void * _udata, ogg_int64_t _offset, int _whence ) -{ - QFile * f = static_cast( _udata ); - - if( _whence == SEEK_CUR ) - { - f->seek( f->pos() + _offset ); - } - else if( _whence == SEEK_END ) - { - f->seek( f->size() + _offset ); - } - else - { - f->seek( _offset ); - } - return 0; -} - - - - -int qfileCloseCallback( void * _udata ) -{ - delete static_cast( _udata ); - return 0; -} - - - - -long qfileTellCallback( void * _udata ) -{ - return static_cast( _udata )->pos(); -} - - - - -f_cnt_t SampleBuffer::decodeSampleOGGVorbis( QString _f, - int_sample_t * & _buf, - ch_cnt_t & _channels, - sample_rate_t & _samplerate ) -{ - static ov_callbacks callbacks = - { - qfileReadCallback, - qfileSeekCallback, - qfileCloseCallback, - qfileTellCallback - } ; - - OggVorbis_File vf; - - f_cnt_t frames = 0; - - QFile * f = new QFile( _f ); - if( f->open( QFile::ReadOnly ) == false ) - { - delete f; - return 0; - } - - int err = ov_open_callbacks( f, &vf, NULL, 0, callbacks ); - - if( err < 0 ) - { - switch( err ) - { - case OV_EREAD: - printf( "SampleBuffer::decodeSampleOGGVorbis():" - " media read error\n" ); - break; - case OV_ENOTVORBIS: -/* printf( "SampleBuffer::decodeSampleOGGVorbis():" - " not an Ogg Vorbis file\n" );*/ - break; - case OV_EVERSION: - printf( "SampleBuffer::decodeSampleOGGVorbis():" - " vorbis version mismatch\n" ); - break; - case OV_EBADHEADER: - printf( "SampleBuffer::decodeSampleOGGVorbis():" - " invalid Vorbis bitstream header\n" ); - break; - case OV_EFAULT: - printf( "SampleBuffer::decodeSampleOgg(): " - "internal logic fault\n" ); - break; - } - delete f; - return 0; - } - - ov_pcm_seek( &vf, 0 ); - - _channels = ov_info( &vf, -1 )->channels; - _samplerate = ov_info( &vf, -1 )->rate; - - ogg_int64_t total = ov_pcm_total( &vf, -1 ); - - _buf = new int_sample_t[total * _channels]; - int bitstream = 0; - long bytes_read = 0; - - do - { - bytes_read = ov_read( &vf, (char *) &_buf[frames * _channels], - ( total - frames ) * _channels * - BYTES_PER_INT_SAMPLE, - isLittleEndian() ? 0 : 1, - BYTES_PER_INT_SAMPLE, 1, &bitstream ); - if( bytes_read < 0 ) - { - break; - } - frames += bytes_read / ( _channels * BYTES_PER_INT_SAMPLE ); - } - while( bytes_read != 0 && bitstream == 0 ); - - ov_clear( &vf ); - // if buffer isn't empty, convert it to float and write it down - - if ( frames > 0 && _buf != NULL ) - { - convertIntToFloat ( _buf, frames, _channels); - } - - return frames; -} -#endif - - - - -f_cnt_t SampleBuffer::decodeSampleDS( QString _f, - int_sample_t * & _buf, - ch_cnt_t & _channels, - sample_rate_t & _samplerate ) -{ - DrumSynth ds; - f_cnt_t frames = ds.GetDSFileSamples( _f, _buf, _channels, _samplerate ); - - if ( frames > 0 && _buf != NULL ) - { - convertIntToFloat ( _buf, frames, _channels); - } - - return frames; - -} - - - - -bool SampleBuffer::play( sampleFrame * _ab, handleState * _state, - const fpp_t _frames, - const float _freq, - const LoopMode _loopmode ) -{ - f_cnt_t startFrame = m_startFrame; - f_cnt_t endFrame = m_endFrame; - f_cnt_t loopStartFrame = m_loopStartFrame; - f_cnt_t loopEndFrame = m_loopEndFrame; - - if( endFrame == 0 || _frames == 0 ) - { + if (endFrame == 0 || _frames == 0) { return false; } // variable for determining if we should currently be playing backwards in a ping-pong loop bool is_backwards = _state->isBackwards(); - const double freq_factor = (double) _freq / (double) m_frequency * - m_sampleRate / Engine::mixer()->processingSampleRate(); + const double freq_factor = (double) _freq / (double) m_data->getFrequency() * + double(m_data->getSampleRate()) / double(Engine::mixer()->processingSampleRate()); // calculate how many frames we have in requested pitch - const f_cnt_t total_frames_for_current_pitch = static_cast( ( - endFrame - startFrame ) / - freq_factor ); + const auto total_frames_for_current_pitch = static_cast((endFrame - startFrame) / + freq_factor ); - if( total_frames_for_current_pitch == 0 ) - { + if (total_frames_for_current_pitch == 0) { return false; } @@ -648,804 +135,320 @@ bool SampleBuffer::play( sampleFrame * _ab, handleState * _state, // this holds the index of the first frame to play f_cnt_t play_frame = qMax(_state->m_frameIndex, startFrame); - if( _loopmode == LoopOff ) - { - if( play_frame >= endFrame || ( endFrame - play_frame ) / freq_factor == 0 ) - { + if (_loopmode == LoopOff) { + if (play_frame >= endFrame || (endFrame - play_frame) / freq_factor == 0) { // the sample is done being played return false; } - } - else if( _loopmode == LoopOn ) - { - play_frame = getLoopedIndex( play_frame, loopStartFrame, loopEndFrame ); - } - else - { - play_frame = getPingPongIndex( play_frame, loopStartFrame, loopEndFrame ); + } else if (_loopmode == LoopOn) { + play_frame = getLoopedIndex(play_frame, loopStartFrame, loopEndFrame); + } else { + play_frame = getPingPongIndex(play_frame, loopStartFrame, loopEndFrame); } - f_cnt_t fragment_size = (f_cnt_t)( _frames * freq_factor ) + MARGIN[ _state->interpolationMode() ]; + f_cnt_t fragment_size = (f_cnt_t) (_frames * freq_factor) + MARGIN[_state->interpolationMode()]; - sampleFrame * tmp = NULL; + sampleFrame *tmp = nullptr; + f_cnt_t usedFrames; // check whether we have to change pitch... - if( freq_factor != 1.0 || _state->m_varyingPitch ) - { + if (freq_factor != 1.0 || _state->m_varyingPitch) { SRC_DATA src_data; // Generate output src_data.data_in = - getSampleFragment( play_frame, fragment_size, _loopmode, &tmp, &is_backwards, - loopStartFrame, loopEndFrame, endFrame )[0]; - src_data.data_out = _ab[0]; + libSampleRateSrc(m_data->getSampleFragment(play_frame, fragment_size, _loopmode, &tmp, &is_backwards, + loopStartFrame, loopEndFrame, endFrame))->data(); + src_data.data_out = _ab->data(); src_data.input_frames = fragment_size; src_data.output_frames = _frames; src_data.src_ratio = 1.0 / freq_factor; src_data.end_of_input = 0; - int error = src_process( _state->m_resamplingData, - &src_data ); - if( error ) - { - printf( "SampleBuffer: error while resampling: %s\n", - src_strerror( error ) ); - } - if( src_data.output_frames_gen > _frames ) - { - printf( "SampleBuffer: not enough frames: %ld / %d\n", - src_data.output_frames_gen, _frames ); + int error = src_process(_state->m_resamplingData, + &src_data); + if (error) { + printf("SampleBuffer: error while resampling: %s\n", + src_strerror(error)); } - // Advance - switch( _loopmode ) - { - case LoopOff: - play_frame += src_data.input_frames_used; - break; - case LoopOn: - play_frame += src_data.input_frames_used; - play_frame = getLoopedIndex( play_frame, loopStartFrame, loopEndFrame ); - break; - case LoopPingPong: - { - f_cnt_t left = src_data.input_frames_used; - if( _state->isBackwards() ) - { - play_frame -= src_data.input_frames_used; - if( play_frame < loopStartFrame ) - { - left -= ( loopStartFrame - play_frame ); - play_frame = loopStartFrame; - } - else left = 0; - } - play_frame += left; - play_frame = getPingPongIndex( play_frame, loopStartFrame, loopEndFrame ); - break; - } + if (src_data.output_frames_gen > _frames) { + printf("SampleBuffer: not enough frames: %ld / %d\n", + src_data.output_frames_gen, _frames); } - } - else - { + + usedFrames = src_data.input_frames_used; + } else { // we don't have to pitch, so we just copy the sample-data // as is into pitched-copy-buffer // Generate output - memcpy( _ab, - getSampleFragment( play_frame, _frames, _loopmode, &tmp, &is_backwards, - loopStartFrame, loopEndFrame, endFrame ), - _frames * BYTES_PER_FRAME ); - // Advance - switch( _loopmode ) - { - case LoopOff: - play_frame += _frames; - break; - case LoopOn: - play_frame += _frames; - play_frame = getLoopedIndex( play_frame, loopStartFrame, loopEndFrame ); - break; - case LoopPingPong: - { - f_cnt_t left = _frames; - if( _state->isBackwards() ) - { - play_frame -= _frames; - if( play_frame < loopStartFrame ) - { - left -= ( loopStartFrame - play_frame ); - play_frame = loopStartFrame; - } - else left = 0; - } - play_frame += left; - play_frame = getPingPongIndex( play_frame, loopStartFrame, loopEndFrame ); - break; + memcpy(_ab, + m_data->getSampleFragment(play_frame, _frames, _loopmode, &tmp, &is_backwards, + loopStartFrame, loopEndFrame, endFrame), + _frames * BYTES_PER_FRAME); + usedFrames = _frames; + } + + // Advance + switch (_loopmode) { + case LoopOff: + play_frame += usedFrames; + break; + case LoopOn: + play_frame += usedFrames; + play_frame = getLoopedIndex(play_frame, loopStartFrame, loopEndFrame); + break; + case LoopPingPong: { + f_cnt_t left = usedFrames; + if (_state->isBackwards()) { + play_frame -= usedFrames; + if (play_frame < loopStartFrame) { + left -= (loopStartFrame - play_frame); + play_frame = loopStartFrame; + } else left = 0; } + play_frame += left; + play_frame = getPingPongIndex(play_frame, loopStartFrame, loopEndFrame); + break; } } - if( tmp != NULL ) - { - MM_FREE( tmp ); + if (tmp != nullptr) { + MM_FREE(tmp); } - _state->setBackwards( is_backwards ); - _state->setFrameIndex( play_frame ); + _state->setBackwards(is_backwards); + _state->setFrameIndex(play_frame); - for( fpp_t i = 0; i < _frames; ++i ) - { - _ab[i][0] *= m_amplification; - _ab[i][1] *= m_amplification; + for (fpp_t i = 0; i < _frames; ++i) { + _ab[i][0] *= m_data->getAmplification(); + _ab[i][1] *= m_data->getAmplification(); } return true; } +f_cnt_t SampleBuffer::getLoopedIndex(f_cnt_t _index, f_cnt_t _startf, f_cnt_t _endf) { + if (_index < _endf) { + return _index; + } + return _startf + (_index - _startf) + % (_endf - _startf); +} -sampleFrame * SampleBuffer::getSampleFragment( f_cnt_t _index, - f_cnt_t _frames, LoopMode _loopmode, sampleFrame * * _tmp, bool * _backwards, - f_cnt_t _loopstart, f_cnt_t _loopend, f_cnt_t _end ) const -{ - if( _loopmode == LoopOff ) - { - if( _index + _frames <= _end ) - { - return m_data + _index; - } - } - else if( _loopmode == LoopOn ) - { - if( _index + _frames <= _loopend ) - { - return m_data + _index; - } - } - else - { - if( ! *_backwards && _index + _frames < _loopend ) - { - return m_data + _index; - } +f_cnt_t SampleBuffer::getPingPongIndex(f_cnt_t _index, f_cnt_t _startf, f_cnt_t _endf) { + if (_index < _endf) { + return _index; } + const f_cnt_t looplen = _endf - _startf; + const f_cnt_t looppos = (_index - _endf) % (looplen * 2); - *_tmp = MM_ALLOC( sampleFrame, _frames ); + return (looppos < looplen) + ? _endf - looppos + : _startf + (looppos - looplen); +} - if( _loopmode == LoopOff ) - { - f_cnt_t available = _end - _index; - memcpy( *_tmp, m_data + _index, available * BYTES_PER_FRAME ); - memset( *_tmp + available, 0, ( _frames - available ) * - BYTES_PER_FRAME ); - } - else if( _loopmode == LoopOn ) - { - f_cnt_t copied = qMin( _frames, _loopend - _index ); - memcpy( *_tmp, m_data + _index, copied * BYTES_PER_FRAME ); - f_cnt_t loop_frames = _loopend - _loopstart; - while( copied < _frames ) - { - f_cnt_t todo = qMin( _frames - copied, loop_frames ); - memcpy( *_tmp + copied, m_data + _loopstart, todo * BYTES_PER_FRAME ); - copied += todo; - } - } - else - { - f_cnt_t pos = _index; - bool backwards = pos < _loopstart - ? false - : *_backwards; - f_cnt_t copied = 0; - - - if( backwards ) - { - copied = qMin( _frames, pos - _loopstart ); - for( int i=0; i < copied; i++ ) - { - (*_tmp)[i][0] = m_data[ pos - i ][0]; - (*_tmp)[i][1] = m_data[ pos - i ][1]; - } - pos -= copied; - if( pos == _loopstart ) backwards = false; - } - else - { - copied = qMin( _frames, _loopend - pos ); - memcpy( *_tmp, m_data + pos, copied * BYTES_PER_FRAME ); - pos += copied; - if( pos == _loopend ) backwards = true; - } - while( copied < _frames ) - { - if( backwards ) - { - f_cnt_t todo = qMin( _frames - copied, pos - _loopstart ); - for ( int i=0; i < todo; i++ ) - { - (*_tmp)[ copied + i ][0] = m_data[ pos - i ][0]; - (*_tmp)[ copied + i ][1] = m_data[ pos - i ][1]; - } - pos -= todo; - copied += todo; - if( pos <= _loopstart ) backwards = false; - } - else - { - f_cnt_t todo = qMin( _frames - copied, _loopend - pos ); - memcpy( *_tmp + copied, m_data + pos, todo * BYTES_PER_FRAME ); - pos += todo; - copied += todo; - if( pos >= _loopend ) backwards = true; - } - } - *_backwards = backwards; - } +void SampleBuffer::visualize(QPainter &_p, const QRect &_dr, + const QRect &_clip, f_cnt_t _from_frame, f_cnt_t _to_frame) { + auto polyPair = visualizeToPoly(_dr, _clip, _from_frame, _to_frame); - return *_tmp; + _p.setRenderHint(QPainter::Antialiasing); + _p.drawPolyline(polyPair.first); + _p.drawPolyline(polyPair.second); } +std::pair SampleBuffer::visualizeToPoly(const QRect &_dr, const QRect &_clip, + f_cnt_t _from_frame, f_cnt_t _to_frame) const { + const int w = _dr.width(); + const int h = _dr.height(); + const bool focus_on_range = _from_frame < _to_frame; + int y_space = (h / 2); + /* Don't visualize while rendering / doing after-rendering changes. */ + return Engine::mixer()->runWhileNotRendering([=]() -> std::pair { + if (m_data->frames() == 0) return {}; - -f_cnt_t SampleBuffer::getLoopedIndex( f_cnt_t _index, f_cnt_t _startf, f_cnt_t _endf ) const -{ - if( _index < _endf ) - { - return _index; - } - return _startf + ( _index - _startf ) - % ( _endf - _startf ); -} + auto to_frame = _to_frame; + if (to_frame > m_data->frames()) + to_frame = m_data->frames(); -f_cnt_t SampleBuffer::getPingPongIndex( f_cnt_t _index, f_cnt_t _startf, f_cnt_t _endf ) const -{ - if( _index < _endf ) - { - return _index; - } - const f_cnt_t looplen = _endf - _startf; - const f_cnt_t looppos = ( _index - _endf ) % ( looplen*2 ); + const int nb_frames = focus_on_range ? to_frame - _from_frame : m_data->frames(); + if (nb_frames == 0) return {}; - return ( looppos < looplen ) - ? _endf - looppos - : _startf + ( looppos - looplen ); -} + const int fpp = tLimit(nb_frames / w, 1, 20); + bool shouldAddAdditionalPoint = (nb_frames % fpp) != 0; + int pointsCount = (nb_frames / fpp) + (shouldAddAdditionalPoint ? 1 : 0); + auto l = QPolygonF(pointsCount); + auto r = QPolygonF(pointsCount); -void SampleBuffer::visualize( QPainter & _p, const QRect & _dr, - const QRect & _clip, f_cnt_t _from_frame, f_cnt_t _to_frame ) -{ - if( m_frames == 0 ) return; + int n = 0; + const int xb = _dr.x(); + const int first = focus_on_range ? _from_frame : 0; + const int last = focus_on_range ? to_frame : m_data->frames(); - const bool focus_on_range = _to_frame <= m_frames - && 0 <= _from_frame && _from_frame < _to_frame; - //_p.setClipRect( _clip ); - const int w = _dr.width(); - const int h = _dr.height(); + int zeroPoint = _dr.y() + y_space; + if (h % 2 != 0) + zeroPoint += 1; + for (int frame = first; frame < last; frame += fpp) { + double x = (xb + (frame - first) * double(w) / nb_frames); - const int yb = h / 2 + _dr.y(); - const float y_space = h*0.5f; - const int nb_frames = focus_on_range ? _to_frame - _from_frame : m_frames; - - const int fpp = tLimit( nb_frames / w, 1, 20 ); - QPointF * l = new QPointF[nb_frames / fpp + 1]; - QPointF * r = new QPointF[nb_frames / fpp + 1]; - int n = 0; - const int xb = _dr.x(); - const int first = focus_on_range ? _from_frame : 0; - const int last = focus_on_range ? _to_frame : m_frames; - for( int frame = first; frame < last; frame += fpp ) - { - l[n] = QPointF( xb + ( (frame - first) * double( w ) / nb_frames ), - ( yb - ( m_data[frame][0] * y_space * m_amplification ) ) ); - r[n] = QPointF( xb + ( (frame - first) * double( w ) / nb_frames ), - ( yb - ( m_data[frame][1] * y_space * m_amplification ) ) ); - ++n; - } - _p.setRenderHint( QPainter::Antialiasing ); - _p.drawPolyline( l, nb_frames / fpp ); - _p.drawPolyline( r, nb_frames / fpp ); - delete[] l; - delete[] r; -} + l[n] = QPointF(x, + (zeroPoint + (m_data->data()[frame][0] * y_space))); + r[n] = QPointF(x, + (zeroPoint + (m_data->data()[frame][1] * y_space))); + ++n; + } + return {std::move(l), std::move(r)}; + }); +} -QString SampleBuffer::openAudioFile() const -{ - FileDialog ofd( NULL, tr( "Open audio file" ) ); +QString SampleBuffer::openAudioFile(const QString ¤tAudioFile) { + FileDialog ofd(NULL, tr("Open audio file")); QString dir; - if( !m_audioFile.isEmpty() ) - { - QString f = m_audioFile; - if( QFileInfo( f ).isRelative() ) - { + if (!currentAudioFile.isEmpty()) { + QString f = currentAudioFile; + if (QFileInfo(f).isRelative()) { f = ConfigManager::inst()->userSamplesDir() + f; - if( QFileInfo( f ).exists() == false ) - { + if (QFileInfo(f).exists() == false) { f = ConfigManager::inst()->factorySamplesDir() + - m_audioFile; + currentAudioFile; } } - dir = QFileInfo( f ).absolutePath(); - } - else - { + dir = QFileInfo(f).absolutePath(); + } else { dir = ConfigManager::inst()->userSamplesDir(); } // change dir to position of previously opened file - ofd.setDirectory( dir ); - ofd.setFileMode( FileDialog::ExistingFiles ); + ofd.setDirectory(dir); + ofd.setFileMode(FileDialog::ExistingFiles); // set filters QStringList types; - types << tr( "All Audio-Files (*.wav *.ogg *.ds *.flac *.spx *.voc " - "*.aif *.aiff *.au *.raw)" ) - << tr( "Wave-Files (*.wav)" ) - << tr( "OGG-Files (*.ogg)" ) - << tr( "DrumSynth-Files (*.ds)" ) - << tr( "FLAC-Files (*.flac)" ) - << tr( "SPEEX-Files (*.spx)" ) - //<< tr( "MP3-Files (*.mp3)" ) - //<< tr( "MIDI-Files (*.mid)" ) - << tr( "VOC-Files (*.voc)" ) - << tr( "AIFF-Files (*.aif *.aiff)" ) - << tr( "AU-Files (*.au)" ) - << tr( "RAW-Files (*.raw)" ) + types << tr("All Audio-Files (*.wav *.ogg *.ds *.flac *.spx *.voc " + "*.aif *.aiff *.au *.raw)") + << tr("Wave-Files (*.wav)") + << tr("OGG-Files (*.ogg)") + << tr("DrumSynth-Files (*.ds)") + << tr("FLAC-Files (*.flac)") + << tr("SPEEX-Files (*.spx)") + //<< tr( "MP3-Files (*.mp3)" ) + //<< tr( "MIDI-Files (*.mid)" ) + << tr("VOC-Files (*.voc)") + << tr("AIFF-Files (*.aif *.aiff)") + << tr("AU-Files (*.au)") + << tr("RAW-Files (*.raw)") //<< tr( "MOD-Files (*.mod)" ) - ; - ofd.setNameFilters( types ); - if( !m_audioFile.isEmpty() ) - { + ; + ofd.setNameFilters(types); + if (!currentAudioFile.isEmpty()) { // select previously opened file - ofd.selectFile( QFileInfo( m_audioFile ).fileName() ); + ofd.selectFile(QFileInfo(currentAudioFile).fileName()); } - if( ofd.exec () == QDialog::Accepted ) - { - if( ofd.selectedFiles().isEmpty() ) - { + if (ofd.exec() == QDialog::Accepted) { + if (ofd.selectedFiles().isEmpty()) { return QString(); } - return tryToMakeRelative( ofd.selectedFiles()[0] ); + return tryToMakeRelative(ofd.selectedFiles()[0]); } return QString(); } +QString SampleBuffer::openAndSetAudioFile(const QString ¤tAudioFile) { + QString fileName = this->openAudioFile(currentAudioFile); - -QString SampleBuffer::openAndSetAudioFile() -{ - QString fileName = this->openAudioFile(); - - if(!fileName.isEmpty()) - { - this->setAudioFile( fileName ); + if (!fileName.isEmpty()) { + this->setAudioFile(fileName); } return fileName; } -QString SampleBuffer::openAndSetWaveformFile() -{ - if( m_audioFile.isEmpty() ) - { - m_audioFile = ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac"; +QString SampleBuffer::openAndSetWaveformFile(QString currentAudioFile) { + if (currentAudioFile.isEmpty()) { + currentAudioFile = ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac"; } - QString fileName = this->openAudioFile(); + QString fileName = this->openAudioFile(currentAudioFile); - if(!fileName.isEmpty()) - { - this->setAudioFile( fileName ); - } - else - { - m_audioFile = ""; + if (!fileName.isEmpty()) { + this->setAudioFile(fileName); } return fileName; } +sample_t SampleBuffer::userWaveSample(const float _sample) const { + auto data = guardedData(); + f_cnt_t dataFrames = data->frames(); + if (dataFrames == 0) + return 0; - -#undef LMMS_HAVE_FLAC_STREAM_ENCODER_H /* not yet... */ -#undef LMMS_HAVE_FLAC_STREAM_DECODER_H - -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H -FLAC__StreamEncoderWriteStatus flacStreamEncoderWriteCallback( - const FLAC__StreamEncoder * - /*_encoder*/, - const FLAC__byte _buffer[], - unsigned int/* _samples*/, - unsigned int _bytes, - unsigned int/* _current_frame*/, - void * _client_data ) -{ -/* if( _bytes == 0 ) - { - return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; - }*/ - return ( static_cast( _client_data )->write( - (const char *) _buffer, _bytes ) == - (int) _bytes ) ? - FLAC__STREAM_ENCODER_WRITE_STATUS_OK : - FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; -} - - -void flacStreamEncoderMetadataCallback( const FLAC__StreamEncoder *, - const FLAC__StreamMetadata * _metadata, - void * _client_data ) -{ - QBuffer * b = static_cast( _client_data ); - b->seek( 0 ); - b->write( (const char *) _metadata, sizeof( *_metadata ) ); -} - -#endif - - - -QString & SampleBuffer::toBase64( QString & _dst ) const -{ -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H - const f_cnt_t FRAMES_PER_BUF = 1152; - - FLAC__StreamEncoder * flac_enc = FLAC__stream_encoder_new(); - FLAC__stream_encoder_set_channels( flac_enc, DEFAULT_CHANNELS ); - FLAC__stream_encoder_set_blocksize( flac_enc, FRAMES_PER_BUF ); -/* FLAC__stream_encoder_set_do_exhaustive_model_search( flac_enc, true ); - FLAC__stream_encoder_set_do_mid_side_stereo( flac_enc, true );*/ - FLAC__stream_encoder_set_sample_rate( flac_enc, - Engine::mixer()->sampleRate() ); - QBuffer ba_writer; - ba_writer.open( QBuffer::WriteOnly ); - - FLAC__stream_encoder_set_write_callback( flac_enc, - flacStreamEncoderWriteCallback ); - FLAC__stream_encoder_set_metadata_callback( flac_enc, - flacStreamEncoderMetadataCallback ); - FLAC__stream_encoder_set_client_data( flac_enc, &ba_writer ); - if( FLAC__stream_encoder_init( flac_enc ) != FLAC__STREAM_ENCODER_OK ) - { - printf( "error within FLAC__stream_encoder_init()!\n" ); - } - f_cnt_t frame_cnt = 0; - while( frame_cnt < m_frames ) - { - f_cnt_t remaining = qMin( FRAMES_PER_BUF, - m_frames - frame_cnt ); - FLAC__int32 buf[FRAMES_PER_BUF * DEFAULT_CHANNELS]; - for( f_cnt_t f = 0; f < remaining; ++f ) - { - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) - { - buf[f*DEFAULT_CHANNELS+ch] = (FLAC__int32)( - Mixer::clip( m_data[f+frame_cnt][ch] ) * - OUTPUT_SAMPLE_MULTIPLIER ); - } - } - FLAC__stream_encoder_process_interleaved( flac_enc, buf, - remaining ); - frame_cnt += remaining; - } - FLAC__stream_encoder_finish( flac_enc ); - FLAC__stream_encoder_delete( flac_enc ); - printf("%d %d\n", frame_cnt, (int)ba_writer.size() ); - ba_writer.close(); - - base64::encode( ba_writer.buffer().data(), ba_writer.buffer().size(), - _dst ); - - -#else /* LMMS_HAVE_FLAC_STREAM_ENCODER_H */ - - base64::encode( (const char *) m_data, - m_frames * sizeof( sampleFrame ), _dst ); - -#endif /* LMMS_HAVE_FLAC_STREAM_ENCODER_H */ - - return _dst; -} - - - - -SampleBuffer * SampleBuffer::resample( const sample_rate_t _src_sr, - const sample_rate_t _dst_sr ) -{ - sampleFrame * data = m_data; - const f_cnt_t frames = m_frames; - const f_cnt_t dst_frames = static_cast( frames / - (float) _src_sr * (float) _dst_sr ); - SampleBuffer * dst_sb = new SampleBuffer( dst_frames ); - sampleFrame * dst_buf = dst_sb->m_origData; - - // yeah, libsamplerate, let's rock with sinc-interpolation! - int error; - SRC_STATE * state; - if( ( state = src_new( SRC_SINC_MEDIUM_QUALITY, - DEFAULT_CHANNELS, &error ) ) != NULL ) - { - SRC_DATA src_data; - src_data.end_of_input = 1; - src_data.data_in = data[0]; - src_data.data_out = dst_buf[0]; - src_data.input_frames = frames; - src_data.output_frames = dst_frames; - src_data.src_ratio = (double) _dst_sr / _src_sr; - if( ( error = src_process( state, &src_data ) ) ) - { - printf( "SampleBuffer: error while resampling: %s\n", - src_strerror( error ) ); - } - src_delete( state ); - } - else - { - printf( "Error: src_new() failed in sample_buffer.cpp!\n" ); - } - dst_sb->update(); - return dst_sb; -} - - - - -void SampleBuffer::setAudioFile( const QString & _audio_file ) -{ - m_audioFile = tryToMakeRelative( _audio_file ); - update(); -} - - - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H - -struct flacStreamDecoderClientData -{ - QBuffer * read_buffer; - QBuffer * write_buffer; -} ; - - - -FLAC__StreamDecoderReadStatus flacStreamDecoderReadCallback( - const FLAC__StreamDecoder * - /*_decoder*/, - FLAC__byte * _buffer, - unsigned int * _bytes, - void * _client_data ) -{ - int res = static_cast( - _client_data )->read_buffer->read( - (char *) _buffer, *_bytes ); - - if( res > 0 ) - { - *_bytes = res; - return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; - - } - *_bytes = 0; - return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; -} - - - - -FLAC__StreamDecoderWriteStatus flacStreamDecoderWriteCallback( - const FLAC__StreamDecoder * - /*_decoder*/, - const FLAC__Frame * _frame, - const FLAC__int32 * const _buffer[], - void * _client_data ) -{ - if( _frame->header.channels != 2 ) - { - printf( "channels != 2 in " - "flacStreamDecoderWriteCallback()\n" ); - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - - if( _frame->header.bits_per_sample != 16 ) - { - printf( "bits_per_sample != 16 in " - "flacStreamDecoderWriteCallback()\n" ); - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - - const f_cnt_t frames = _frame->header.blocksize; - for( f_cnt_t frame = 0; frame < frames; ++frame ) - { - sampleFrame sframe = { _buffer[0][frame] / - OUTPUT_SAMPLE_MULTIPLIER, - _buffer[1][frame] / - OUTPUT_SAMPLE_MULTIPLIER - } ; - static_cast( - _client_data )->write_buffer->write( - (const char *) sframe, sizeof( sframe ) ); + const float frame = _sample * dataFrames; + f_cnt_t f1 = static_cast( frame ) % dataFrames; + if (f1 < 0) { + f1 += dataFrames; } - return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; -} - -void flacStreamDecoderMetadataCallback( const FLAC__StreamDecoder *, - const FLAC__StreamMetadata *, - void * /*_client_data*/ ) -{ - printf("stream decoder metadata callback\n"); -/* QBuffer * b = static_cast( _client_data ); - b->seek( 0 ); - b->write( (const char *) _metadata, sizeof( *_metadata ) );*/ + return linearInterpolate(data->data()[f1][0], data->data()[(f1 + 1) % dataFrames][0], fraction(frame)); } -void flacStreamDecoderErrorCallback( const FLAC__StreamDecoder *, - FLAC__StreamDecoderErrorStatus _status, - void * /*_client_data*/ ) -{ - printf("error callback! %d\n", _status); - // what to do now?? -} - -#endif - - -void SampleBuffer::loadFromBase64( const QString & _data ) -{ - char * dst = NULL; - int dsize = 0; - base64::decode( _data, &dst, &dsize ); - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H - - QByteArray orig_data = QByteArray::fromRawData( dst, dsize ); - QBuffer ba_reader( &orig_data ); - ba_reader.open( QBuffer::ReadOnly ); - - QBuffer ba_writer; - ba_writer.open( QBuffer::WriteOnly ); - - flacStreamDecoderClientData cdata = { &ba_reader, &ba_writer } ; - - FLAC__StreamDecoder * flac_dec = FLAC__stream_decoder_new(); - - FLAC__stream_decoder_set_read_callback( flac_dec, - flacStreamDecoderReadCallback ); - FLAC__stream_decoder_set_write_callback( flac_dec, - flacStreamDecoderWriteCallback ); - FLAC__stream_decoder_set_error_callback( flac_dec, - flacStreamDecoderErrorCallback ); - FLAC__stream_decoder_set_metadata_callback( flac_dec, - flacStreamDecoderMetadataCallback ); - FLAC__stream_decoder_set_client_data( flac_dec, &cdata ); - - FLAC__stream_decoder_init( flac_dec ); - - FLAC__stream_decoder_process_until_end_of_stream( flac_dec ); - - FLAC__stream_decoder_finish( flac_dec ); - FLAC__stream_decoder_delete( flac_dec ); - - ba_reader.close(); - - orig_data = ba_writer.buffer(); - printf("%d\n", (int) orig_data.size() ); - - m_origFrames = orig_data.size() / sizeof( sampleFrame ); - MM_FREE( m_origData ); - m_origData = MM_ALLOC( sampleFrame, m_origFrames ); - memcpy( m_origData, orig_data.data(), orig_data.size() ); - -#else /* LMMS_HAVE_FLAC_STREAM_DECODER_H */ - - m_origFrames = dsize / sizeof( sampleFrame ); - MM_FREE( m_origData ); - m_origData = MM_ALLOC( sampleFrame, m_origFrames ); - memcpy( m_origData, dst, dsize ); - -#endif - - delete[] dst; - - m_audioFile = QString(); - update(); -} - +QString &SampleBuffer::toBase64(QString &_dst) const { + auto data = guardedData(); + base64::encode((const char *) data->data(), + data->frames() * sizeof(sampleFrame), _dst); - -void SampleBuffer::setStartFrame( const f_cnt_t _s ) -{ - m_startFrame = _s; -} - - - - -void SampleBuffer::setEndFrame( const f_cnt_t _e ) -{ - m_endFrame = _e; -} - - - - -void SampleBuffer::setAmplification( float _a ) -{ - m_amplification = _a; - emit sampleUpdated(); -} - - - - -void SampleBuffer::setReversed( bool _on ) -{ - m_reversed = _on; - update( true ); + return _dst; } - - - -QString SampleBuffer::tryToMakeRelative( const QString & file ) -{ - if( QFileInfo( file ).isRelative() == false ) - { +QString SampleBuffer::tryToMakeRelative(const QString &file) { + if (QFileInfo(file).isRelative() == false) { // Normalize the path - QString f( QDir::cleanPath( file ) ); + QString f(QDir::cleanPath(file)); // First, look in factory samples // Isolate "samples/" from "data:/samples/" - QString samplesSuffix = ConfigManager::inst()->factorySamplesDir().mid( ConfigManager::inst()->dataDir().length() ); + QString samplesSuffix = ConfigManager::inst()->factorySamplesDir().mid( + ConfigManager::inst()->dataDir().length()); // Iterate over all valid "data:/" searchPaths - for ( const QString & path : QDir::searchPaths( "data" ) ) - { - QString samplesPath = QDir::cleanPath( path + samplesSuffix ) + "/"; - if ( f.startsWith( samplesPath ) ) - { - return QString( f ).mid( samplesPath.length() ); + for (const QString &path : QDir::searchPaths("data")) { + QString samplesPath = QDir::cleanPath(path + samplesSuffix) + "/"; + if (f.startsWith(samplesPath)) { + return QString(f).mid(samplesPath.length()); } } // Next, look in user samples QString usd = ConfigManager::inst()->userSamplesDir(); - usd.replace( QDir::separator(), '/' ); - if( f.startsWith( usd ) ) - { - return QString( f ).mid( usd.length() ); + usd.replace(QDir::separator(), '/'); + if (f.startsWith(usd)) { + return QString(f).mid(usd.length()); } } return file; } - - -QString SampleBuffer::tryToMakeAbsolute(const QString& file) -{ +QString SampleBuffer::tryToMakeAbsolute(const QString &file) { QFileInfo f(file); - if(f.isRelative()) - { + if (f.isRelative()) { f = QFileInfo(ConfigManager::inst()->userSamplesDir() + file); - if(! f.exists()) - { + if (!f.exists()) { f = QFileInfo(ConfigManager::inst()->factorySamplesDir() + file); } } @@ -1456,31 +459,69 @@ QString SampleBuffer::tryToMakeAbsolute(const QString& file) return file; } +SampleBuffer::handleState::handleState(bool _varying_pitch, int interpolation_mode) : + m_frameIndex(0), + m_varyingPitch(_varying_pitch), + m_isBackwards(false) { + int error; + m_interpolationMode = interpolation_mode; + if ((m_resamplingData = src_new(interpolation_mode, DEFAULT_CHANNELS, &error)) == NULL) { + qDebug("Error: src_new() failed in sample_buffer.cpp!\n"); + } +} +SampleBuffer::handleState::~handleState() { + src_delete(m_resamplingData); +} +SampleBuffer::DataChangeHelper::DataChangeHelper(SampleBuffer *buffer, SampleBuffer::UpdateType updateType) + :m_buffer{buffer}, m_updateType{updateType} +{ +} +SampleBuffer::DataChangeHelper::~DataChangeHelper() { + *m_buffer->m_playInfo = internal::SampleBufferPlayInfo(m_buffer->m_data->frames()); + m_buffer->m_infoChangeNotifier->onValueUpdated(m_buffer->createInfo()); + emit m_buffer->sampleUpdated(m_updateType); +} -SampleBuffer::handleState::handleState( bool _varying_pitch, int interpolation_mode ) : - m_frameIndex( 0 ), - m_varyingPitch( _varying_pitch ), - m_isBackwards( false ) -{ - int error; - m_interpolationMode = interpolation_mode; - - if( ( m_resamplingData = src_new( interpolation_mode, DEFAULT_CHANNELS, &error ) ) == NULL ) - { - qDebug( "Error: src_new() failed in sample_buffer.cpp!\n" ); - } +void SampleBuffer::addData(const SampleBuffer::DataVector &vector, sample_rate_t sampleRate) { + // First of all, don't let anyone read. + DataChangeHelper helper(this, UpdateType::Append); + + m_data->addData(vector, sampleRate); } +void SampleBuffer::resetData(DataVector &&newData, sample_rate_t dataSampleRate) { + DataChangeHelper helper(this, UpdateType::Clear); + m_data->resetData(std::move(newData), dataSampleRate); +} +void SampleBuffer::reverse() { + DataChangeHelper helper(this, UpdateType::Clear); + m_data->reverse(); +} +void SampleBuffer::setAudioFile(const QString &audioFile, bool ignoreError) +{ + *this = SampleBuffer(audioFile, ignoreError); +} + +SampleBuffer::SampleBuffer() + : SampleBuffer{internal::SampleBufferData{}} +{ +} -SampleBuffer::handleState::~handleState() +SampleBuffer::SampleBuffer(internal::SampleBufferData &&data) + : m_data{std::make_shared(std::move(data))}, + m_playInfo(std::make_shared(m_data->frames())) { - src_delete( m_resamplingData ); + emit sampleUpdated(UpdateType::Clear); +} + +SampleBuffer::SampleBuffer(const QString &base64Data, sample_rate_t sample_rate) + : SampleBuffer{internal::SampleBufferData::loadFromBase64(base64Data, sample_rate)} { } diff --git a/src/core/SampleBufferVisualizer.cpp b/src/core/SampleBufferVisualizer.cpp new file mode 100644 index 00000000000..99f6d486779 --- /dev/null +++ b/src/core/SampleBufferVisualizer.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2018 Shmuel H. (shmuelhazan0/at/gmail.com) + * + * This file is part of LMMS - https://lmms.io + * + * 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 2 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ +#include "include/SampleBufferVisualizer.h" + +#include "Track.h" +#include "Engine.h" + +#include +#include + +SampleBufferVisualizer::SampleBufferVisualizer() +{ + +} + +void SampleBufferVisualizer::update(const SampleBuffer &sampleBuffer, + MidiTime sampleStartOffset, + MidiTime sampleLength, + const QRect &parentRect, + float pixelsPerTact, + f_cnt_t framesPerTact, + const QPen &pen, + SampleBufferVisualizer::Operation operation) +{ + // != + if (pixelsPerTact < m_pixelsPerTact || pixelsPerTact > m_pixelsPerTact + || framesPerTact != m_framesPerTact + || sampleLength < m_cachedTime) + operation = Operation::Clear; + + m_pixelsPerTact = pixelsPerTact; + m_generalPaintOffset = sampleStartOffset; + m_framesPerTact = framesPerTact; + m_drawPixelOffset = parentRect.left(); + + switch (operation) { + case Operation::Clear: + m_cachedPixmaps.clear(); + m_currentPixmap.pixmap = QPixmap(); + m_currentPixmap.totalTime = 0; + m_cachedTime = 0; + +#ifdef Q_FALLTHROUGH + Q_FALLTHROUGH(); +#endif + case Operation::Append: + appendMultipleTacts(sampleBuffer, + sampleLength, + parentRect.translated(-parentRect.x(), + 0), + pen); + break; + } +} + +void SampleBufferVisualizer::draw(QPainter &painter) +{ + float pixelOffset = pixelsPerTime(m_generalPaintOffset) + m_drawPixelOffset; + for (const auto &pixmap : m_cachedPixmaps) { + auto targetRect = pixmap.rect.translated(int(pixelOffset), + 0); + + painter.drawPixmap(targetRect, pixmap.pixmap); + pixelOffset += pixelsPerTime(pixmap.totalTime); + } + + auto targetRect = m_currentPixmap.rect.translated(int(pixelOffset), + 0); + painter.drawPixmap(targetRect, m_currentPixmap.pixmap); +} + +void SampleBufferVisualizer::appendMultipleTacts(const SampleBuffer &sampleBuffer, + MidiTime sampleLength, + const QRect &parentRect, + const QPen &pen) +{ + for (; (m_cachedTime+m_currentPixmap.totalTime) < sampleLength;) + { + MidiTime totalTime; + MidiTime offsetFromTact = m_currentPixmap.totalTime; + bool isCompleteTact = false; + + // we have more tacts to draw. finish the current one. + if (MidiTime(m_cachedTime+offsetFromTact).getTact() < sampleLength.getTact()) { + // Paint what left from the current tact. + totalTime = MidiTime::ticksPerTact() - offsetFromTact; + isCompleteTact = true; + } else { + // Draw only the ticks left in the current tact. + totalTime = sampleLength - m_cachedTime - m_currentPixmap.totalTime; + } + + Q_ASSERT((offsetFromTact + totalTime) <= MidiTime::ticksPerTact()); + + if (pixelsPerTime(totalTime) < 1) { + // We can't paint it. + // totalTime is too short. skip it. + // or just wait until we have enough frames. + if (isCompleteTact) { + // Skip it and continue to the next tact. + m_currentPixmap.totalTime += totalTime; + } else { + // Wait until we have enough frames. + break; + } + } + + auto result = appendTact(sampleBuffer, + totalTime, + parentRect, + pen, + isCompleteTact); + if (! result) + break; + + if (isCompleteTact) { + m_cachedTime += ( m_currentPixmap.totalTime ); + m_cachedPixmaps.push_back(std::move(m_currentPixmap)); + m_currentPixmap.pixmap = QPixmap(); + m_currentPixmap.totalTime = 0; + } + } +} + +bool SampleBufferVisualizer::appendTact(const SampleBuffer &sampleBuffer, + const MidiTime &totalTime, + const QRect &parentRect, + const QPen &pen, + bool isLastInTact) +{ + auto offsetFromTact = m_currentPixmap.totalTime; + + auto currentPaintInTact = getRectForSampleFragment (parentRect, + offsetFromTact, + totalTime, + isLastInTact); + Q_ASSERT(currentPaintInTact.width() > 0); + + // Generate the actual visualization. + auto fromFrame = MidiTime(m_cachedTime + offsetFromTact).frames (m_framesPerTact); + + auto poly = sampleBuffer.visualizeToPoly (currentPaintInTact, + QRect(), + fromFrame, + fromFrame + totalTime.frames(m_framesPerTact)); + + + m_currentPixmap.totalTime += totalTime; + + m_currentPixmap.rect = getRectForSampleFragment (parentRect, + 0, + MidiTime::ticksPerTact()); + if (m_currentPixmap.pixmap.isNull()) { + m_currentPixmap.pixmap = QPixmap(m_currentPixmap.rect.size()); + m_currentPixmap.pixmap.fill(Qt::transparent); + } + + // Draw the points into the pixmap. + QPainter pixmapPainter (&m_currentPixmap.pixmap); + pixmapPainter.setPen(pen); + + pixmapPainter.setRenderHint( QPainter::Antialiasing ); + + pixmapPainter.drawPolyline (poly.first); + pixmapPainter.drawPolyline (poly.second); + pixmapPainter.end(); + + // Continue to the next tact or stop. + return true; +} + +QRect SampleBufferVisualizer::getRectForSampleFragment(QRect parentRect, MidiTime beginOffset, + MidiTime totalTime, + bool forceNotZeroWidth) { + int offset = pixelsPerTime(beginOffset); + + float top = parentRect.top (); + float height = parentRect.height (); + + QRect r = QRect( int(parentRect.x ()) + int(offset), + top, + int(qMax( int(pixelsPerTime(totalTime)) , (forceNotZeroWidth ? 1 : 0) )), + int(height)); + + + return r; +} diff --git a/src/core/SamplePlayHandle.cpp b/src/core/SamplePlayHandle.cpp index cae2f3cde01..381e2b816c6 100644 --- a/src/core/SamplePlayHandle.cpp +++ b/src/core/SamplePlayHandle.cpp @@ -32,9 +32,10 @@ -SamplePlayHandle::SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPort ) : +SamplePlayHandle::SamplePlayHandle(const std::shared_ptr &sampleBuffer, bool ownAudioPort) : PlayHandle( TypeSamplePlayHandle ), - m_sampleBuffer( sharedObject::ref( sampleBuffer ) ), + m_sampleBuffer( sampleBuffer ), + m_sampleBufferInfo(m_sampleBuffer->createInfo()), m_doneMayReturnTrue( true ), m_frame( 0 ), m_ownAudioPort( ownAudioPort ), @@ -53,9 +54,8 @@ SamplePlayHandle::SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPo SamplePlayHandle::SamplePlayHandle( const QString& sampleFile ) : - SamplePlayHandle( new SampleBuffer( sampleFile ) , true) + SamplePlayHandle( std::make_shared( sampleFile, false ) , true) { - sharedObject::unref( m_sampleBuffer ); } @@ -73,7 +73,6 @@ SamplePlayHandle::SamplePlayHandle( SampleTCO* tco ) : SamplePlayHandle::~SamplePlayHandle() { - sharedObject::unref( m_sampleBuffer ); if( m_ownAudioPort ) { delete audioPort(); @@ -141,7 +140,10 @@ bool SamplePlayHandle::isFromTrack( const Track * _track ) const f_cnt_t SamplePlayHandle::totalFrames() const { - return ( m_sampleBuffer->endFrame() - m_sampleBuffer->startFrame() ) * ( Engine::mixer()->processingSampleRate() / Engine::mixer()->baseSampleRate() ); + f_cnt_t total_frames = ( m_sampleBufferInfo.endFrame - m_sampleBufferInfo.startFrame ); + qreal processingToSampleRateRatio = static_cast(Engine::mixer()->processingSampleRate()) / static_cast(m_sampleBufferInfo.sampleRate); + + return static_cast(total_frames * processingToSampleRateRatio); } diff --git a/src/core/SampleRecordHandle.cpp b/src/core/SampleRecordHandle.cpp index cf9342b9e08..0cacaee50c0 100644 --- a/src/core/SampleRecordHandle.cpp +++ b/src/core/SampleRecordHandle.cpp @@ -29,38 +29,54 @@ #include "InstrumentTrack.h" #include "Mixer.h" #include "SampleBuffer.h" -#include "SampleTrack.h" #include "debug.h" -SampleRecordHandle::SampleRecordHandle( SampleTCO* tco ) : +SampleRecordHandle::SampleRecordHandle(SampleTCO* tco , MidiTime startRecordTimeOffset) : PlayHandle( TypeSamplePlayHandle ), m_framesRecorded( 0 ), - m_minLength( tco->length() ), m_track( tco->getTrack() ), m_bbTrack( NULL ), - m_tco( tco ) + m_tco( tco ), + m_recordingChannel{dynamic_cast(m_track)->recordingChannel ()}, + m_startRecordTimeOffset{startRecordTimeOffset} { + m_tco->setIsRecording(true); } SampleRecordHandle::~SampleRecordHandle() -{ - if( !m_buffers.empty() ) - { - SampleBuffer* sb; - createSampleBuffer( &sb ); - m_tco->setSampleBuffer( sb ); +{ + // No problem to block here. + // this is not a renderer thread anymore + // so it will not cause a deadlock + // with requestChangesInModel. + + // Wait for the current async rendering job + // to finish. + m_lastAsyncWork.waitForFinished(); + + if (! m_currentBuffer.empty()) { + // We have data that has not been written into the buffer. + // force-write it into the buffer. + + // Add a new rendering job and wait for it to finish. + addOrCreateBuffer(); + m_lastAsyncWork.waitForFinished(); } - - while( !m_buffers.empty() ) - { - delete[] m_buffers.front().first; - m_buffers.erase( m_buffers.begin() ); + + m_tco->updateLength (); + + // If this is an automatically created tco, + // enable resizing. + if (m_framesRecorded != 0) { + m_tco->setAutoResize (false); + m_tco->setRecord( false ); } - m_tco->setRecord( false ); + + m_tco->setIsRecording(false); } @@ -70,15 +86,16 @@ void SampleRecordHandle::play( sampleFrame * /*_working_buffer*/ ) { const sampleFrame * recbuf = Engine::mixer()->inputBuffer(); const f_cnt_t frames = Engine::mixer()->inputBufferFrames(); + writeBuffer( recbuf, frames ); - m_framesRecorded += frames; - MidiTime len = (tick_t)( m_framesRecorded / Engine::framesPerTick() ); - if( len > m_minLength ) - { -// m_tco->changeLength( len ); - m_minLength = len; - } + // Add data to the buffer. + if (m_lastAsyncWork.isFinished()) + addOrCreateBuffer(); + + m_framesRecorded += frames; + m_timeRecorded = m_framesRecorded / Engine::framesPerTick (Engine::mixer ()->inputSampleRate ()); + m_currentBuffer.clear(); } @@ -86,7 +103,7 @@ void SampleRecordHandle::play( sampleFrame * /*_working_buffer*/ ) bool SampleRecordHandle::isFinished() const { - return false; + return !m_tco->getAutoResize () && (m_startRecordTimeOffset + m_timeRecorded) >= m_tco->length (); } @@ -105,32 +122,60 @@ f_cnt_t SampleRecordHandle::framesRecorded() const return( m_framesRecorded ); } - - - -void SampleRecordHandle::createSampleBuffer( SampleBuffer** sampleBuf ) +void SampleRecordHandle::copyBufferFromMonoLeft(const sampleFrame *inputBuffer, + sampleFrame *outputBuffer, + const f_cnt_t _frames) { - const f_cnt_t frames = framesRecorded(); - // create buffer to store all recorded buffers in - sampleFrame * data = new sampleFrame[frames]; - // make sure buffer is cleaned up properly at the end... - sampleFrame * data_ptr = data; - + for( f_cnt_t frame = 0; frame < _frames; ++frame ) { + // Copy every first sample to the first and the second in the output buffer. + outputBuffer[frame][LEFT_CHANNEL_INDEX] = inputBuffer[frame][LEFT_CHANNEL_INDEX]; + outputBuffer[frame][RIGHT_CHANNEL_INDEX] = inputBuffer[frame][LEFT_CHANNEL_INDEX]; + } +} - assert( data != NULL ); +void SampleRecordHandle::copyBufferFromMonoRight(const sampleFrame *inputBuffer, + sampleFrame *outputBuffer, + const f_cnt_t _frames) +{ + for( f_cnt_t frame = 0; frame < _frames; ++frame ) { + // Copy every second sample to the first and the second in the output buffer. + outputBuffer[frame][LEFT_CHANNEL_INDEX] = inputBuffer[frame][RIGHT_CHANNEL_INDEX]; + outputBuffer[frame][RIGHT_CHANNEL_INDEX] = inputBuffer[frame][RIGHT_CHANNEL_INDEX]; + } +} - // now copy all buffers into big buffer - for( bufferList::const_iterator it = m_buffers.begin(); - it != m_buffers.end(); ++it ) +void SampleRecordHandle::copyBufferFromStereo(const sampleFrame *inputBuffer, + sampleFrame *outputBuffer, + const f_cnt_t _frames) +{ + for( f_cnt_t frame = 0; frame < _frames; ++frame ) { - memcpy( data_ptr, ( *it ).first, ( *it ).second * - sizeof( sampleFrame ) ); - data_ptr += ( *it ).second; + for( ch_cnt_t chnl = 0; chnl < DEFAULT_CHANNELS; ++chnl ) + { + outputBuffer[frame][chnl] = inputBuffer[frame][chnl]; + } } - // create according sample-buffer out of big buffer - *sampleBuf = new SampleBuffer( data, frames ); - ( *sampleBuf)->setSampleRate( Engine::mixer()->inputSampleRate() ); - delete[] data; +} + +void SampleRecordHandle::addOrCreateBuffer() { + auto sampleBuffer = m_tco->sampleBuffer (); + auto currentBufferCopy = m_currentBuffer; + m_lastAsyncWork = runAsync(std::bind([this, currentBufferCopy, sampleBuffer] () mutable{ + if (m_framesRecorded == 0) { + // Protect m_tco->setStartTimeOffset; + auto guard = Engine::mixer()->requestChangesGuard(); + // Make sure we don't have the previous data. + sampleBuffer->resetData(std::move (currentBufferCopy), + Engine::mixer ()->inputSampleRate ()); + m_tco->setStartTimeOffset (m_startRecordTimeOffset); + } else { + if (! currentBufferCopy.empty ()) { + sampleBuffer->addData(currentBufferCopy, + Engine::mixer ()->inputSampleRate ()); + } + } + })); + } @@ -139,15 +184,32 @@ void SampleRecordHandle::createSampleBuffer( SampleBuffer** sampleBuf ) void SampleRecordHandle::writeBuffer( const sampleFrame * _ab, const f_cnt_t _frames ) { - sampleFrame * buf = new sampleFrame[_frames]; - for( f_cnt_t frame = 0; frame < _frames; ++frame ) - { - for( ch_cnt_t chnl = 0; chnl < DEFAULT_CHANNELS; ++chnl ) - { - buf[frame][chnl] = _ab[frame][chnl]; - } + auto framesInBuffer = m_currentBuffer.size(); + // Add _frames elements to the buffer. + m_currentBuffer.resize (framesInBuffer + _frames); + + // Depending on the recording channel, copy the buffer as a + // mono-right, mono-left or stereo. + + // Note that mono doesn't mean single channel, it means + // empty other channel. Therefore, we would just duplicate + // every frame from the mono channel + // to the empty channel. + + switch(m_recordingChannel) { + case SampleTrack::RecordingChannel::MonoLeft: + copyBufferFromMonoLeft(_ab, m_currentBuffer.data () + framesInBuffer, _frames); + break; + case SampleTrack::RecordingChannel::MonoRight: + copyBufferFromMonoRight(_ab, m_currentBuffer.data () + framesInBuffer, _frames); + break; + case SampleTrack::RecordingChannel::Stereo: + copyBufferFromStereo(_ab, m_currentBuffer.data () + framesInBuffer, _frames); + break; + default: + Q_ASSERT(false); + break; } - m_buffers.push_back( qMakePair( buf, _frames ) ); } diff --git a/src/core/Song.cpp b/src/core/Song.cpp index 2809c61adb1..b3593e40f3a 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -108,6 +108,7 @@ Song::Song() : this, SLOT( masterPitchChanged() ) );*/ qRegisterMetaType( "Note" ); + qRegisterMetaType("MidiTime"); setType( SongContainer ); } @@ -345,6 +346,11 @@ void Song::processNextBuffer() m_vstSyncController.setPlaybackJumped( true ); + if (isRecording()) + { + emit beforeRecordOn(tl->loopBegin()); + } + emit updateSampleTracks(); } } @@ -555,6 +561,17 @@ void Song::playAndRecord() { playSong(); m_recording = true; + + MidiTime time = getPlayPos(); + auto tl = getPlayPos().m_timeLine; + bool checkLoop = + tl != NULL && tl->loopPointsEnabled(); + + if (checkLoop && (time >= tl->loopEnd() || time < tl->loopBegin())) { + time = tl->loopBegin(); + } + + emit beforeRecordOn(time); } diff --git a/src/core/Track.cpp b/src/core/Track.cpp index 64c17c9e8ec..ca63328613d 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -241,6 +241,15 @@ void TrackContentObject::setStartTimeOffset( const MidiTime &startTimeOffset ) m_startTimeOffset = startTimeOffset; } +bool TrackContentObject::isRecording() const +{ + return m_isRecording; +} + +void TrackContentObject::setIsRecording(bool value) +{ + m_isRecording = value; +} @@ -368,6 +377,9 @@ QColor TrackContentObjectView::mutedColor() const QColor TrackContentObjectView::mutedBackgroundColor() const { return m_mutedBackgroundColor; } +QColor TrackContentObjectView::recordingBackgroundColor() const +{ return m_recordingBackgroundColor; } + QColor TrackContentObjectView::selectedColor() const { return m_selectedColor; } @@ -412,6 +424,9 @@ void TrackContentObjectView::setTextShadowColor( const QColor & c ) void TrackContentObjectView::setBBPatternBackground( const QColor & c ) { m_BBPatternBackground = QColor( c ); } +void TrackContentObjectView::setRecordingBackgroundColor (const QColor & c ) +{ m_recordingBackgroundColor = QColor( c ); } + void TrackContentObjectView::setGradient( const bool & b ) { m_gradient = b; } @@ -512,7 +527,6 @@ void TrackContentObjectView::updatePosition() } - /*! \brief Change the trackContentObjectView's display when something * being dragged enters it. * @@ -711,6 +725,10 @@ void TrackContentObjectView::paintTextLabel(QString const & text, QPainter & pai */ void TrackContentObjectView::mousePressEvent( QMouseEvent * me ) { + // Disallow changes to a track that is being recorded into. + if (m_tco->isRecording()) + return; + setInitialMousePos( me->pos() ); if( !fixedTCOs() && me->button() == Qt::LeftButton ) { @@ -725,7 +743,7 @@ void TrackContentObjectView::mousePressEvent( QMouseEvent * me ) m_action = ToggleSelected; } } - else if( !me->modifiers() ) + else if( !me->modifiers()) { if( isSelected() ) { @@ -982,7 +1000,9 @@ void TrackContentObjectView::mouseMoveEvent( QMouseEvent * me ) m_tco->movePosition( t ); m_trackView->getTrackContentWidget()->changePosition(); m_tco->changeLength( m_tco->length() + ( oldPos - t ) ); - sTco->setStartTimeOffset( sTco->startTimeOffset() + ( oldPos - t ) ); + + if (! sTco->isEmpty ()) + sTco->setStartTimeOffset( sTco->startTimeOffset() + ( oldPos - t ) ); } } } @@ -1884,6 +1904,11 @@ void TrackOperationsWidget::clearTrack() t->unlock(); } +QPushButton *TrackOperationsWidget::trackOps() const +{ + return m_trackOps; +} + /*! \brief Remove this track from the track list @@ -1907,65 +1932,9 @@ void TrackOperationsWidget::removeTrack() */ void TrackOperationsWidget::updateMenu() { - QMenu * toMenu = m_trackOps->menu(); - toMenu->clear(); - toMenu->addAction( embed::getIconPixmap( "edit_copy", 16, 16 ), - tr( "Clone this track" ), - this, SLOT( cloneTrack() ) ); - toMenu->addAction( embed::getIconPixmap( "cancel", 16, 16 ), - tr( "Remove this track" ), - this, SLOT( removeTrack() ) ); - - if( ! m_trackView->trackContainerView()->fixedTCOs() ) - { - toMenu->addAction( tr( "Clear this track" ), this, SLOT( clearTrack() ) ); - } - if (QMenu *fxMenu = m_trackView->createFxMenu(tr("FX %1: %2"), tr("Assign to new FX Channel"))) - { - toMenu->addMenu(fxMenu); - } - - if (InstrumentTrackView * trackView = dynamic_cast(m_trackView)) - { - toMenu->addSeparator(); - toMenu->addMenu(trackView->midiMenu()); - } - if( dynamic_cast( m_trackView ) ) - { - toMenu->addAction( tr( "Turn all recording on" ), this, SLOT( recordingOn() ) ); - toMenu->addAction( tr( "Turn all recording off" ), this, SLOT( recordingOff() ) ); - } -} - - -void TrackOperationsWidget::toggleRecording( bool on ) -{ - AutomationTrackView * atv = dynamic_cast( m_trackView ); - if( atv ) - { - for( TrackContentObject * tco : atv->getTrack()->getTCOs() ) - { - AutomationPattern * ap = dynamic_cast( tco ); - if( ap ) { ap->setRecording( on ); } - } - atv->update(); - } -} - - - -void TrackOperationsWidget::recordingOn() -{ - toggleRecording( true ); -} - - -void TrackOperationsWidget::recordingOff() -{ - toggleRecording( false ); + return m_trackView->updateTrackOperationsWidgetMenu (this); } - // =========================================================================== // track // =========================================================================== @@ -2242,9 +2211,10 @@ void Track::loadSettings( const QDomElement & element ) * * \param tco The TrackContentObject to attach to this track. */ -TrackContentObject * Track::addTCO( TrackContentObject * tco ) -{ - m_trackContentObjects.push_back( tco ); +TrackContentObject *Track::addTCO(TrackContentObject *tco) { + auto guard = Engine::mixer()->requestChangesGuard(); + + m_trackContentObjects.push_back(tco); emit trackContentObjectAdded( tco ); @@ -2258,16 +2228,15 @@ TrackContentObject * Track::addTCO( TrackContentObject * tco ) * * \param tco The TrackContentObject to remove from this track. */ -void Track::removeTCO( TrackContentObject * tco ) -{ - tcoVector::iterator it = std::find( m_trackContentObjects.begin(), - m_trackContentObjects.end(), - tco ); - if( it != m_trackContentObjects.end() ) - { - m_trackContentObjects.erase( it ); - if( Engine::getSong() ) - { +void Track::removeTCO(TrackContentObject *tco) { + auto guard = Engine::mixer()->requestChangesGuard(); + + tcoVector::iterator it = std::find(m_trackContentObjects.begin(), + m_trackContentObjects.end(), + tco); + if (it != m_trackContentObjects.end()) { + m_trackContentObjects.erase(it); + if (Engine::getSong()) { Engine::getSong()->updateLength(); Engine::getSong()->setModified(); } @@ -2276,10 +2245,9 @@ void Track::removeTCO( TrackContentObject * tco ) /*! \brief Remove all TCOs from this track */ -void Track::deleteTCOs() -{ - while( ! m_trackContentObjects.isEmpty() ) - { +void Track::deleteTCOs() { + auto guard = Engine::mixer()->requestChangesGuard(); + while (!m_trackContentObjects.isEmpty()) { delete m_trackContentObjects.first(); } } @@ -2552,6 +2520,11 @@ BoolModel *Track::getMutedModel() return &m_mutedModel; } +TrackContentObject *Track::createTCO(const MidiTime &pos) { + auto guard = Engine::mixer()->requestChangesGuard(); + return unsafeCreateTCO(pos); +} + @@ -2672,6 +2645,29 @@ void TrackView::update() QWidget::update(); } +void TrackView::updateTrackOperationsWidgetMenu(TrackOperationsWidget *trackOperations) +{ + QMenu * toMenu = trackOperations->m_trackOps->menu(); + + toMenu->clear(); + toMenu->addAction( embed::getIconPixmap( "edit_copy", 16, 16 ), + tr( "Clone this track" ), + trackOperations, SLOT( cloneTrack() ) ); + toMenu->addAction( embed::getIconPixmap( "cancel", 16, 16 ), + tr( "Remove this track" ), + trackOperations, SLOT( removeTrack() ) ); + + if( ! trackContainerView()->fixedTCOs() ) + { + toMenu->addAction( tr( "Clear this track" ), trackOperations, SLOT( clearTrack() ) ); + } + + if (QMenu *fxMenu = createFxMenu(tr("FX %1: %2"), tr("Assign to new FX Channel"))) + { + toMenu->addMenu(fxMenu); + } +} + diff --git a/src/core/audio/AudioDevice.cpp b/src/core/audio/AudioDevice.cpp index ab69e144b12..efb933a3249 100644 --- a/src/core/audio/AudioDevice.cpp +++ b/src/core/audio/AudioDevice.cpp @@ -196,8 +196,8 @@ void AudioDevice::resample( const surroundSampleFrame * _src, } m_srcData.input_frames = _frames; m_srcData.output_frames = _frames; - m_srcData.data_in = (float *) _src[0]; - m_srcData.data_out = _dst[0]; + m_srcData.data_in = (float *) _src[0].data (); + m_srcData.data_out = _dst[0].data (); m_srcData.src_ratio = (double) _dst_sr / _src_sr; m_srcData.end_of_input = 0; int error; diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index bca41356b93..b6ab9698e59 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -52,10 +52,13 @@ AudioJack::AudioJack( bool & _success_ful, Mixer* _mixer ) : m_active( false ), m_midiClient( NULL ), m_tempOutBufs( new jack_default_audio_sample_t *[channels()] ), + m_tempInBufs( new jack_default_audio_sample_t *[channels()] ), m_outBuf( new surroundSampleFrame[mixer()->framesPerPeriod()] ), m_framesDoneInCurBuf( 0 ), m_framesToDoInCurBuf( 0 ) { + m_supportsCapture = true; + _success_ful = initJackClient(); if( _success_ful ) { @@ -88,6 +91,7 @@ AudioJack::~AudioJack() jack_client_close( m_client ); } + delete[] m_tempInBufs; delete[] m_tempOutBufs; delete[] m_outBuf; @@ -188,7 +192,24 @@ bool AudioJack::initJackClient() JackPortIsOutput, 0 ) ); if( m_outputPorts.back() == NULL ) { - printf( "no more JACK-ports available!\n" ); + printf( "no more out JACK-ports available!\n" ); + return false; + } + } + + // Register In ports + for( ch_cnt_t ch = 0; ch < channels(); ++ch ) + { + QString name = QString( "master in " ) + + ( ( ch % 2 ) ? "R" : "L" ) + + QString::number( ch / 2 + 1 ); + m_inputPorts.push_back( jack_port_register( m_client, + name.toLatin1().constData(), + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0 ) ); + if( m_inputPorts.back() == NULL ) + { + printf( "no more in JACK-ports available!\n" ); return false; } } @@ -222,30 +243,30 @@ void AudioJack::startProcessing() - const char * * ports = jack_get_ports( m_client, NULL, NULL, + const char * * inputPorts = jack_get_ports( m_client, NULL, NULL, JackPortIsPhysical | JackPortIsInput ); - if( ports == NULL ) - { - printf( "no physical playback ports. you'll have to do " - "connections at your own!\n" ); + // Connect lmms to the default playback device. + if (inputPorts) { + for (uint i = 0; i < channels (); ++i) { + connectJackPort (jack_port_name (m_outputPorts[i]), inputPorts[i], "playback"); + } + + free( inputPorts ); } - else - { - for( ch_cnt_t ch = 0; ch < channels(); ++ch ) - { - if( jack_connect( m_client, jack_port_name( - m_outputPorts[ch] ), - ports[ch] ) ) - { - printf( "cannot connect output ports. you'll " - "have to do connections at your own!\n" - ); - } + + const char * * outputPorts = jack_get_ports( m_client, NULL, NULL, + JackPortIsPhysical | + JackPortIsOutput ); + // Connect lmms to the default capture device. + if (outputPorts) { + for (uint i = 0; i < channels (); ++i) { + connectJackPort (outputPorts[i], jack_port_name (m_inputPorts[i]), "capture"); } + + free( outputPorts ); } - free( ports ); } @@ -354,6 +375,10 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) m_tempOutBufs[c] = (jack_default_audio_sample_t *) jack_port_get_buffer( m_outputPorts[c], _nframes ); + m_tempInBufs[c] = + (jack_default_audio_sample_t *) jack_port_get_buffer( + m_inputPorts[c], _nframes ); + } #ifdef AUDIO_PORT_SUPPORT @@ -409,6 +434,21 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) } } + if (! m_stopped) { + m_inBuffer.resize (_nframes); + + for( int c = 0; c < channels(); ++c ) { + jack_default_audio_sample_t *channel_buffer = m_tempInBufs[c]; + + for( jack_nframes_t frame = 0; frame < _nframes; ++frame ) + { + m_inBuffer[frame][c] = channel_buffer[frame]; + } + } + + mixer()->pushInputFrames (m_inBuffer.data (), _nframes); + } + if( _nframes != done ) { for( int c = 0; c < channels(); ++c ) @@ -440,6 +480,27 @@ void AudioJack::shutdownCallback( void * _udata ) _this->zombified(); } +void AudioJack::connectJackPort(const char *outputPort, + const char *inputPort, + const char *portName) { + if(outputPort == NULL || inputPort == NULL) + { + printf( "no physical %s port. you'll have to do " + "connections at your own!\n", portName); + } + else + { + if( jack_connect( m_client, outputPort, + inputPort) ) + { + printf( "cannot connect %s port. you'll " + "have to do connections at your own!\n", + portName); + } + } + +} + diff --git a/src/core/audio/AudioPortAudio.cpp b/src/core/audio/AudioPortAudio.cpp index 5566d7a3634..26103ed6236 100644 --- a/src/core/audio/AudioPortAudio.cpp +++ b/src/core/audio/AudioPortAudio.cpp @@ -64,6 +64,7 @@ AudioPortAudio::AudioPortAudio( bool & _success_ful, Mixer * _mixer ) : m_outBufPos( 0 ) { _success_ful = false; + m_supportsCapture = true; m_outBufSize = mixer()->framesPerPeriod(); @@ -170,9 +171,6 @@ AudioPortAudio::AudioPortAudio( bool & _success_ful, Mixer * _mixer ) : printf( "Input device: '%s' backend: '%s'\n", Pa_GetDeviceInfo( inDevIdx )->name, Pa_GetHostApiInfo( Pa_GetDeviceInfo( inDevIdx )->hostApi )->name ); printf( "Output device: '%s' backend: '%s'\n", Pa_GetDeviceInfo( outDevIdx )->name, Pa_GetHostApiInfo( Pa_GetDeviceInfo( outDevIdx )->hostApi )->name ); - // TODO: debug Mixer::pushInputFrames() - //m_supportsCapture = true; - _success_ful = true; } @@ -260,7 +258,7 @@ int AudioPortAudio::process_callback( float * _outputBuffer, unsigned long _framesPerBuffer ) { - if( supportsCapture() ) + if( supportsCapture() && _inputBuffer) { mixer()->pushInputFrames( (sampleFrame*)_inputBuffer, _framesPerBuffer ); diff --git a/src/core/audio/AudioPulseAudio.cpp b/src/core/audio/AudioPulseAudio.cpp index 20a106ed116..1a8f70f2960 100644 --- a/src/core/audio/AudioPulseAudio.cpp +++ b/src/core/audio/AudioPulseAudio.cpp @@ -42,7 +42,10 @@ static void stream_write_callback(pa_stream *s, size_t length, void *userdata) } - +static void stream_read_callback(pa_stream *s, size_t length, void *userdata) +{ + static_cast( userdata )->streamReadCallback( s, length ); +} AudioPulseAudio::AudioPulseAudio( bool & _success_ful, Mixer* _mixer ) : AudioDevice( tLimit( @@ -50,15 +53,22 @@ AudioPulseAudio::AudioPulseAudio( bool & _success_ful, Mixer* _mixer ) : DEFAULT_CHANNELS, SURROUND_CHANNELS ), _mixer ), m_s( NULL ), - m_quit( false ), - m_convertEndian( false ) + m_recordStream{nullptr}, + m_quit( false ) { _success_ful = false; - m_sampleSpec.format = PA_SAMPLE_S16LE; + setSampleRate (Engine::mixer ()->baseSampleRate ()); + m_sampleSpec.format = PA_SAMPLE_FLOAT32; m_sampleSpec.rate = sampleRate(); m_sampleSpec.channels = channels(); + m_recordSampleSpec = m_sampleSpec; + + // It does work. But it has clicking noises every few seconds. + // have no idea way. + m_supportsCapture = false; + _success_ful = true; } @@ -145,6 +155,27 @@ static void stream_state_callback( pa_stream *s, void * userdata ) } } +/* This routine is called whenever the stream state changes */ +static void record_stream_state_callback( pa_stream *s, void * userdata ) +{ + switch( pa_stream_get_state( s ) ) + { + case PA_STREAM_CREATING: + case PA_STREAM_TERMINATED: + break; + + case PA_STREAM_READY: + qDebug( "Record stream successfully created\n" ); + break; + + case PA_STREAM_FAILED: + default: + qCritical( "record stream errror: %s\n", + pa_strerror(pa_context_errno( + pa_stream_get_context( s ) ) ) ); + } +} + /* This is called whenever the context status changes */ @@ -165,6 +196,10 @@ static void context_state_callback(pa_context *c, void *userdata) pa_stream_set_state_callback( _this->m_s, stream_state_callback, _this ); pa_stream_set_write_callback( _this->m_s, stream_write_callback, _this ); + _this->m_recordStream = pa_stream_new( c, "lmms sample track record", &_this->m_sampleSpec, NULL); + pa_stream_set_state_callback( _this->m_recordStream, record_stream_state_callback, _this ); + pa_stream_set_read_callback ( _this->m_recordStream, stream_read_callback , _this ); + pa_buffer_attr buffer_attr; buffer_attr.maxlength = (uint32_t)(-1); @@ -182,10 +217,17 @@ static void context_state_callback(pa_context *c, void *userdata) buffer_attr.tlength = pa_usec_to_bytes( latency * PA_USEC_PER_MSEC, &_this->m_sampleSpec ); - pa_stream_connect_playback( _this->m_s, NULL, &buffer_attr, + pa_stream_connect_playback( _this->m_s, + NULL, + &buffer_attr, PA_STREAM_ADJUST_LATENCY, NULL, // volume NULL ); + + pa_stream_connect_record (_this->m_recordStream, + NULL, + &buffer_attr, + PA_STREAM_ADJUST_LATENCY); _this->signalConnected( true ); break; } @@ -242,6 +284,8 @@ void AudioPulseAudio::run() pa_stream_disconnect( m_s ); pa_stream_unref( m_s ); + pa_stream_disconnect( m_recordStream ); + pa_stream_unref( m_recordStream ); } else { @@ -266,10 +310,9 @@ void AudioPulseAudio::streamWriteCallback( pa_stream *s, size_t length ) { const fpp_t fpp = mixer()->framesPerPeriod(); surroundSampleFrame * temp = new surroundSampleFrame[fpp]; - int_sample_t* pcmbuf = (int_sample_t *)pa_xmalloc( fpp * channels() * sizeof(int_sample_t) ); size_t fd = 0; - while( fd < length/4 && m_quit == false ) + while( fd < length && m_quit == false ) { const fpp_t frames = getNextBuffer( temp ); if( !frames ) @@ -277,24 +320,48 @@ void AudioPulseAudio::streamWriteCallback( pa_stream *s, size_t length ) m_quit = true; break; } - int bytes = convertToS16( temp, frames, - mixer()->masterGain(), - pcmbuf, - m_convertEndian ); - if( bytes > 0 ) - { - pa_stream_write( m_s, pcmbuf, bytes, NULL, 0, - PA_SEEK_RELATIVE ); + + auto gain = mixer()->masterGain(); + for (fpp_t f = 0; f < frames; ++f) { + temp[f][0] *= gain; + temp[f][1] *= gain; } - fd += frames; + + + + pa_stream_write( m_s, static_cast(temp), + frames * sizeof(surroundSampleFrame), NULL, 0, + PA_SEEK_RELATIVE ); + + fd += (frames * sizeof(surroundSampleFrame)); } - pa_xfree( pcmbuf ); delete[] temp; } +void AudioPulseAudio::streamReadCallback(pa_stream *s, size_t length) { + const sampleFrame *buffer = nullptr; + size_t buffer_size = 0; + + while (length > 0) { + + pa_stream_peek (m_recordStream, + reinterpret_cast (const_cast(&buffer)), + &buffer_size); + + fpp_t frames = buffer_size / sizeof (sampleFrame); + + pa_stream_drop (m_recordStream); + if (buffer_size && buffer) { + mixer()->pushInputFrames (buffer, + frames); + } + + length -= buffer_size; + } +} void AudioPulseAudio::signalConnected( bool connected ) { diff --git a/src/core/audio/AudioSampleRecorder.cpp b/src/core/audio/AudioSampleRecorder.cpp deleted file mode 100644 index a36e0e9da06..00000000000 --- a/src/core/audio/AudioSampleRecorder.cpp +++ /dev/null @@ -1,114 +0,0 @@ -/* - * AudioSampleRecorder.cpp - device-class that implements recording - * audio-buffers into RAM - * - * Copyright (c) 2004-2014 Tobias Doerffel - * - * This file is part of LMMS - https://lmms.io - * - * 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 2 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 (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - - -#include "AudioSampleRecorder.h" -#include "SampleBuffer.h" -#include "debug.h" - - - -AudioSampleRecorder::AudioSampleRecorder( const ch_cnt_t _channels, - bool & _success_ful, - Mixer * _mixer ) : - AudioDevice( _channels, _mixer ), - m_buffers() -{ - _success_ful = true; -} - - - - -AudioSampleRecorder::~AudioSampleRecorder() -{ - while( !m_buffers.empty() ) - { - delete[] m_buffers.front().first; - m_buffers.erase( m_buffers.begin() ); - } -} - - - - -f_cnt_t AudioSampleRecorder::framesRecorded() const -{ - f_cnt_t frames = 0; - for( BufferList::ConstIterator it = m_buffers.begin(); - it != m_buffers.end(); ++it ) - { - frames += ( *it ).second; - } - return frames; -} - - - - -void AudioSampleRecorder::createSampleBuffer( SampleBuffer** sampleBuf ) -{ - const f_cnt_t frames = framesRecorded(); - // create buffer to store all recorded buffers in - sampleFrame * data = new sampleFrame[frames]; - // make sure buffer is cleaned up properly at the end... - sampleFrame * data_ptr = data; - - - assert( data != NULL ); - - // now copy all buffers into big buffer - for( BufferList::ConstIterator it = m_buffers.begin(); - it != m_buffers.end(); ++it ) - { - memcpy( data_ptr, ( *it ).first, ( *it ).second * - sizeof( sampleFrame ) ); - data_ptr += ( *it ).second; - } - // create according sample-buffer out of big buffer - *sampleBuf = new SampleBuffer( data, frames ); - ( *sampleBuf )->setSampleRate( sampleRate() ); - delete[] data; -} - - - - -void AudioSampleRecorder::writeBuffer( const surroundSampleFrame * _ab, - const fpp_t _frames, const float ) -{ - sampleFrame * buf = new sampleFrame[_frames]; - for( fpp_t frame = 0; frame < _frames; ++frame ) - { - for( ch_cnt_t chnl = 0; chnl < DEFAULT_CHANNELS; ++chnl ) - { - buf[frame][chnl] = _ab[frame][chnl]; - } - } - m_buffers.push_back( qMakePair( buf, _frames ) ); -} - - - diff --git a/src/core/audio/AudioSdl.cpp b/src/core/audio/AudioSdl.cpp index 42adb9b33c9..45e6e043a22 100644 --- a/src/core/audio/AudioSdl.cpp +++ b/src/core/audio/AudioSdl.cpp @@ -99,7 +99,9 @@ AudioSdl::AudioSdl( bool & _success_ful, Mixer* _mixer ) : _success_ful = true; #ifdef LMMS_HAVE_SDL2 +#ifdef LMMS_BUILD_WIN32 m_inputAudioHandle = m_audioHandle; + m_inputAudioHandle.freq = mixer ()->inputSampleRate (); m_inputAudioHandle.callback = sdlInputAudioCallback; m_inputDevice = SDL_OpenAudioDevice (NULL, @@ -107,13 +109,16 @@ AudioSdl::AudioSdl( bool & _success_ful, Mixer* _mixer ) : &m_inputAudioHandle, &actual, 0); - if (m_inputDevice != 0) { + if (m_inputDevice != 0 && actual.freq == mixer ()->inputSampleRate ()) { m_supportsCapture = true; } else { m_supportsCapture = false; qWarning ( "Couldn't open SDL capture device: %s\n", SDL_GetError ()); } - +#else + qWarning("SDL: Recording has been disabled on Linux since it " + "appears to have a growing latency"); +#endif #endif } diff --git a/src/core/samplebuffer/SampleBufferData.cpp b/src/core/samplebuffer/SampleBufferData.cpp new file mode 100644 index 00000000000..3cc9afc4e31 --- /dev/null +++ b/src/core/samplebuffer/SampleBufferData.cpp @@ -0,0 +1,153 @@ +#include +#include +#include +#include "internal/SampleBufferData.h" + +internal::SampleBufferData::SampleBufferData(internal::SampleBufferData::DataVector &&data, + sample_rate_t sampleRate) + : m_data{std::move(data)}, m_frequency{BaseFreq}, m_sampleRate{sampleRate} { +} + +internal::SampleBufferData +internal::SampleBufferData::loadFromBase64(const QString &_data, sample_rate_t sampleRate) { + char *dst = nullptr; + int dsize = 0; + base64::decode(_data, &dst, &dsize); + + SampleBufferData::DataVector input(dsize / sizeof(sampleFrame)); + memcpy(input.data(), + dst, + input.size() * sizeof(sampleFrame)); + + delete[] dst; + + return SampleBufferData(std::move(input), sampleRate); +} + + +internal::SampleBufferData::DataVector +internal::SampleBufferData::resampleData(const SampleBufferData::DataVector &inputData, sample_rate_t inputSampleRate, + sample_rate_t desiredSampleRate) { + if (inputData.empty()) { + // no need to resample empty data + return DataVector{}; + } + const auto dst_frames = static_cast( inputData.size() / + (float) inputSampleRate * (float) desiredSampleRate ); + DataVector outputData(dst_frames); + + // yeah, libsamplerate, let's rock with sinc-interpolation! + int error; + SRC_STATE *state; + if ((state = src_new(SRC_SINC_MEDIUM_QUALITY, + DEFAULT_CHANNELS, &error)) != nullptr) { + SRC_DATA src_data; + src_data.end_of_input = 1; + src_data.data_in = libSampleRateSrc(inputData.data())->data(); + src_data.data_out = outputData.data()->data(); + src_data.input_frames = inputData.size(); + src_data.output_frames = dst_frames; + src_data.src_ratio = (double) desiredSampleRate / inputSampleRate; + if ((error = src_process(state, &src_data))) { + printf("SampleBuffer: error while resampling: %s\n", + src_strerror(error)); + } + src_delete(state); + } else { + printf("Error: src_new() failed in sample_buffer.cpp!\n"); + } + + return outputData; +} + + + + + + + +const +sampleFrame *internal::SampleBufferData::getSampleFragment(f_cnt_t _index, + f_cnt_t _frames, LoopMode _loopmode, sampleFrame **_tmp, + bool *_backwards, + f_cnt_t _loopstart, f_cnt_t _loopend, f_cnt_t _end) const { + if (_loopmode == LoopOff) { + if (_index + _frames <= _end) { + return data() + _index; + } + } else if (_loopmode == LoopOn) { + if (_index + _frames <= _loopend) { + return data() + _index; + } + } else { + if (!*_backwards && _index + _frames < _loopend) { + return data() + _index; + } + } + + *_tmp = MM_ALLOC(sampleFrame, _frames); + + if (_loopmode == LoopOff) { + f_cnt_t available = _end - _index; + memcpy(*_tmp, data() + _index, available * BYTES_PER_FRAME); + memset(*_tmp + available, 0, (_frames - available) * + BYTES_PER_FRAME); + } else if (_loopmode == LoopOn) { + f_cnt_t copied = qMin(_frames, _loopend - _index); + memcpy(*_tmp, data() + _index, copied * BYTES_PER_FRAME); + f_cnt_t loop_frames = _loopend - _loopstart; + while (copied < _frames) { + f_cnt_t todo = qMin(_frames - copied, loop_frames); + memcpy(*_tmp + copied, data() + _loopstart, todo * BYTES_PER_FRAME); + copied += todo; + } + } else { + f_cnt_t pos = _index; + bool backwards = pos < _loopstart + ? false + : *_backwards; + f_cnt_t copied = 0; + + + if (backwards) { + copied = qMin(_frames, pos - _loopstart); + for (int i = 0; i < copied; i++) { + (*_tmp)[i][0] = m_data[pos - i][0]; + (*_tmp)[i][1] = m_data[pos - i][1]; + } + pos -= copied; + if (pos == _loopstart) backwards = false; + } else { + copied = qMin(_frames, _loopend - pos); + memcpy(*_tmp, data() + pos, copied * BYTES_PER_FRAME); + pos += copied; + if (pos == _loopend) backwards = true; + } + + while (copied < _frames) { + if (backwards) { + f_cnt_t todo = qMin(_frames - copied, pos - _loopstart); + for (int i = 0; i < todo; i++) { + (*_tmp)[copied + i][0] = m_data[pos - i][0]; + (*_tmp)[copied + i][1] = m_data[pos - i][1]; + } + pos -= todo; + copied += todo; + if (pos <= _loopstart) backwards = false; + } else { + f_cnt_t todo = qMin(_frames - copied, _loopend - pos); + memcpy(*_tmp + copied, data() + pos, todo * BYTES_PER_FRAME); + pos += todo; + copied += todo; + if (pos >= _loopend) backwards = true; + } + } + *_backwards = backwards; + } + + return *_tmp; +} + + + + diff --git a/src/core/samplebuffer/SampleBufferFileHelper.cpp b/src/core/samplebuffer/SampleBufferFileHelper.cpp new file mode 100644 index 00000000000..79ac3a2921b --- /dev/null +++ b/src/core/samplebuffer/SampleBufferFileHelper.cpp @@ -0,0 +1,349 @@ +#include "internal/SampleBufferFileHelper.h" + +#include + + +#include +#include +#include +#include "DrumSynth.h" +#include "FileDialog.h" + + +#ifdef LMMS_HAVE_OGGVORBIS +#define OV_EXCLUDE_STATIC_CALLBACKS + +#include + +#endif + +#include "internal/SampleBufferFileHelper.h" +#include "GuiApplication.h" +#include "ConfigManager.h" +#include "Mixer.h" +#include "endian_handling.h" + + +namespace internal { + // TODO: move these to interfaces. + // (for another commit) + typedef SampleBufferData::DataVector (FileDecoderFunc)(const QString &fileName, + ch_cnt_t &_channels, sample_rate_t &_samplerate, + QString &loadingWarning, bool &isError); + + namespace FileDecoders { + FileDecoderFunc decodeSampleSF; + FileDecoderFunc decodeSampleOGGVorbis; + FileDecoderFunc decodeSampleDS; + } + + + constexpr FileDecoderFunc *fileDecodingFunctions[] = { + FileDecoders::decodeSampleSF, + FileDecoders::decodeSampleOGGVorbis, + FileDecoders::decodeSampleDS + }; + + SampleBufferData::DataVector convertIntToFloat(int_sample_t *&_ibuf, f_cnt_t _frames, int _channels); +} + +internal::SampleBufferData::DataVector internal::FileDecoders::decodeSampleSF(const QString &_f, + ch_cnt_t &_channels, + sample_rate_t &_samplerate, + QString &loadingWarning, + bool &isError) { + SNDFILE *snd_file; + SF_INFO sf_info; + sf_info.format = 0; + f_cnt_t frames = 0; + SampleBufferData::DataVector vector; + bool sf_rr = false; + + + // Use QFile to handle unicode file names on Windows + QFile f(_f); + f.open(QIODevice::ReadOnly); + if ((snd_file = sf_open_fd(f.handle(), SFM_READ, &sf_info, false)) != nullptr) { + frames = sf_info.frames; + const auto channels = sf_info.channels; + + // TODO: remove that. + MmAllocator::vector buffer(channels * frames); + sf_rr = sf_read_float(snd_file, buffer.data(), channels * frames); + + if (sf_info.channels > DEFAULT_CHANNELS) { + loadingWarning = QObject::tr("The file you've selected has %1 channels. LMMS support " + "Stereo and Mono.").arg(sf_info.channels); + } + + // Copy buffer using stereo + vector.resize(frames); + auto rightOffset = sf_info.channels > 1 ? 1 : 0; + for (size_t i = 0; i < frames; i++) { + vector[i][0] = buffer[i * channels]; + vector[i][1] = buffer[i * channels + rightOffset]; + } + + if (sf_rr < sf_info.channels * frames) { +#ifdef DEBUG_LMMS + qDebug( "SampleBuffer::decodeSampleSF(): could not read" + " sample %s: %s", _f, sf_strerror( NULL ) ); +#endif + } + _channels = sf_info.channels; + _samplerate = sf_info.samplerate; + + sf_close(snd_file); + } else { +#ifdef DEBUG_LMMS + qDebug( "SampleBuffer::decodeSampleSF(): could not load " + "sample %s: %s", _f, sf_strerror( NULL ) ); +#endif + loadingWarning = QObject::tr("SoundFile: Could not load: %1").arg(sf_strerror(nullptr)); + isError = true; + } + f.close(); + + return vector; +} + + +#ifdef LMMS_HAVE_OGGVORBIS + +// callback-functions for reading ogg-file + +size_t qfileReadCallback(void *_ptr, size_t _size, size_t _n, void *_udata) { + return static_cast(static_cast( _udata )->read((char *) _ptr, + _size * _n)); +} + + +int qfileSeekCallback(void *_udata, ogg_int64_t _offset, int _whence) { + auto *f = static_cast( _udata ); + + if (_whence == SEEK_CUR) { + f->seek(f->pos() + _offset); + } else if (_whence == SEEK_END) { + f->seek(f->size() + _offset); + } else { + f->seek(_offset); + } + return 0; +} + + +int qfileCloseCallback(void *_udata) { + delete static_cast( _udata ); + return 0; +} + + +long qfileTellCallback(void *_udata) { + return static_cast( _udata )->pos(); +} + + +internal::SampleBufferData::DataVector internal::FileDecoders::decodeSampleOGGVorbis(const QString &_f, + ch_cnt_t &_channels, + sample_rate_t &_samplerate, + QString &loadingWarning, + bool &isError) { + static ov_callbacks callbacks = + { + qfileReadCallback, + qfileSeekCallback, + qfileCloseCallback, + qfileTellCallback + }; + + OggVorbis_File vf; + + f_cnt_t frames = 0; + + auto *f = new QFile(_f); + if (!f->open(QFile::ReadOnly)) { + delete f; + isError = true; + return {}; + } + + int err = ov_open_callbacks(f, &vf, nullptr, 0, callbacks); + + if (err < 0) { + switch (err) { + case OV_EREAD: + printf("SampleBuffer::decodeSampleOGGVorbis():" + " media read error\n"); + break; + case OV_ENOTVORBIS: +/* printf( "SampleBuffer::decodeSampleOGGVorbis():" + " not an Ogg Vorbis file\n" );*/ + break; + case OV_EVERSION: + printf("SampleBuffer::decodeSampleOGGVorbis():" + " vorbis version mismatch\n"); + break; + case OV_EBADHEADER: + printf("SampleBuffer::decodeSampleOGGVorbis():" + " invalid Vorbis bitstream header\n"); + break; + case OV_EFAULT: + printf("SampleBuffer::decodeSampleOgg(): " + "internal logic fault\n"); + break; + } + delete f; + + isError = true; + return {}; + } + + ov_pcm_seek(&vf, 0); + + _channels = ov_info(&vf, -1)->channels; + _samplerate = ov_info(&vf, -1)->rate; + + ogg_int64_t total = ov_pcm_total(&vf, -1); + + auto _buf = new int_sample_t[total * _channels]; + int bitstream = 0; + long bytes_read = 0; + + do { + bytes_read = ov_read(&vf, (char *) &_buf[frames * _channels], + (total - frames) * _channels * + BYTES_PER_INT_SAMPLE, + isLittleEndian() ? 0 : 1, + BYTES_PER_INT_SAMPLE, 1, &bitstream); + if (bytes_read < 0) { + break; + } + frames += bytes_read / (_channels * BYTES_PER_INT_SAMPLE); + } while (bytes_read != 0 && bitstream == 0); + + ov_clear(&vf); + // if buffer isn't empty, convert it to float and write it down + + if (frames > 0) { + return convertIntToFloat(_buf, frames, _channels); + } else if (frames < 0) { + isError = true; + } + + return {}; +} + +#endif + +internal::SampleBufferData::DataVector +internal::FileDecoders::decodeSampleDS(const QString &_f, ch_cnt_t &_channels, + sample_rate_t &_samplerate, + QString &loadingWarning, + bool &isError) { + DrumSynth ds; + int_sample_t *_buf = nullptr; + f_cnt_t frames = ds.GetDSFileSamples(_f, _buf, _channels, _samplerate); + + if (frames > 0 && _buf != nullptr) { + return convertIntToFloat(_buf, frames, _channels); + } + + isError = true; + return {}; +} + +internal::SampleBufferData +internal::SampleBufferFileHelper::Load(internal::SampleBufferFileHelper::FileName fileName, bool ignoreError) { + fileName = tryToMakeRelative(fileName); + + sample_rate_t sampleRate = mixerSampleRate(); + if (fileName == "") + return {{}, sampleRate}; + + bool fileLoadError = false; + + ch_cnt_t channels = DEFAULT_CHANNELS; + + const QFileInfo fileInfo(fileName); + + SampleBufferData::DataVector fileData; + QString loadingWarning; + if (fileInfo.suffix() == "ogg") { + fileLoadError = false; + fileData = FileDecoders::decodeSampleOGGVorbis(fileName, channels, sampleRate, loadingWarning, + fileLoadError); + } + for (auto function : fileDecodingFunctions) { + fileData = function(fileName, channels, sampleRate, loadingWarning, fileLoadError); + if (!fileLoadError) + break; + } + + if (fileLoadError) { + if (!ignoreError) { + QString title = QObject::tr("Fail to open file"); + QString message = QObject::tr("No message"); + if (!loadingWarning.isEmpty()) + message = loadingWarning; + if (gui) { + QMessageBox::information(nullptr, + title, message, QMessageBox::Warning); + } else { + fprintf(stderr, "%s\n", message.toUtf8().constData()); + } + } + + return {{}, mixerSampleRate()}; + } else { + return {std::move(fileData), sampleRate}; + } +} + +QString internal::SampleBufferFileHelper::tryToMakeRelative(const QString &file) { + if (!QFileInfo(file).isRelative()) { + // Normalize the path + QString f(QDir::cleanPath(file)); + + // First, look in factory samples + // Isolate "samples/" from "data:/samples/" + QString samplesSuffix = ConfigManager::inst()->factorySamplesDir().mid( + ConfigManager::inst()->dataDir().length()); + + // Iterate over all valid "data:/" searchPaths + for (const QString &path : QDir::searchPaths("data")) { + QString samplesPath = QDir::cleanPath(path + samplesSuffix) + "/"; + if (f.startsWith(samplesPath)) { + return QString(f).mid(samplesPath.length()); + } + } + + // Next, look in user samples + QString usd = ConfigManager::inst()->userSamplesDir(); + usd.replace(QDir::separator(), '/'); + if (f.startsWith(usd)) { + return QString(f).mid(usd.length()); + } + } + return file; +} + +internal::SampleBufferData::DataVector +internal::convertIntToFloat(int_sample_t *&_ibuf, f_cnt_t _frames, int _channels) { + // following code transforms int-samples into + // float-samples and does amplifying & reversing + const float fac = 1 / OUTPUT_SAMPLE_MULTIPLIER; + SampleBufferData::DataVector vector(_frames); + const int ch = (_channels > 1) ? 1 : 0; + + int idx = 0; + for (f_cnt_t frame = 0; frame < _frames; + ++frame) { + vector[frame][0] = _ibuf[idx + 0] * fac; + vector[frame][1] = _ibuf[idx + ch] * fac; + idx += _channels; + } + + delete[] _ibuf; + + return vector; +} diff --git a/src/gui/LfoControllerDialog.cpp b/src/gui/LfoControllerDialog.cpp index 0a6a8edc7e0..78db0c1d740 100644 --- a/src/gui/LfoControllerDialog.cpp +++ b/src/gui/LfoControllerDialog.cpp @@ -227,13 +227,13 @@ LfoControllerDialog::~LfoControllerDialog() void LfoControllerDialog::askUserDefWave() { - SampleBuffer * sampleBuffer = dynamic_cast(this->model())-> - m_userDefSampleBuffer; + SampleBuffer * sampleBuffer = m_lfo->m_userDefSampleBuffer; + QString fileName = sampleBuffer->openAndSetWaveformFile(); if( fileName.isEmpty() == false ) { // TODO: - ToolTip::add( m_userWaveBtn, sampleBuffer->audioFile() ); + ToolTip::add( m_userWaveBtn, fileName ); } } diff --git a/src/gui/editors/Editor.cpp b/src/gui/editors/Editor.cpp index c27eda4c06c..4da639e3c7a 100644 --- a/src/gui/editors/Editor.cpp +++ b/src/gui/editors/Editor.cpp @@ -25,6 +25,8 @@ #include "Editor.h" #include "Song.h" +#include "ComboBox.h" +#include "ToolButton.h" #include "MainWindow.h" #include "embed.h" @@ -107,10 +109,15 @@ Editor::Editor(bool record, bool stepRecord) : // Add actions to toolbar addButton(m_playAction, "playButton"); + addButton(m_stopAction, "stopButton"); + + // Seperate playback buttons and recording buttons. + m_toolBar->addSeparator (); + if (record) { - addButton(m_recordAction, "recordButton"); addButton(m_recordAccompanyAction, "recordAccompanyButton"); + addButton(m_recordAction, "m_recordAction"); } if(stepRecord) { diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index b397434b102..b0361245b11 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include "AutomatableSlider.h" #include "ComboBox.h" @@ -658,6 +659,8 @@ SongEditorWindow::SongEditorWindow(Song* song) : m_editor(new SongEditor(song)), m_crtlAction( NULL ) { + bool isRecordSupported = Engine::mixer()->audioDev()->supportsCapture(); + setWindowTitle( tr( "Song-Editor" ) ); setWindowIcon( embed::getIconPixmap( "songeditor" ) ); @@ -669,10 +672,42 @@ SongEditorWindow::SongEditorWindow(Song* song) : // Set up buttons m_playAction->setToolTip(tr("Play song (Space)")); + + // Remove the record action. m_recordAction->setToolTip(tr("Record samples from Audio-device")); + m_toolBar->removeAction (m_recordAction); + m_recordAccompanyAction->setToolTip(tr( "Record samples from Audio-device while playing song or BB track")); m_stopAction->setToolTip(tr( "Stop song (Space)" )); + if (isRecordSupported) { + auto *recordMenu = new QMenu(this); + auto *recordGroup = new QActionGroup(this); + recordGroup->addAction (new QAction(tr("Stereo"), recordGroup))->setData (SampleTrack::RecordingChannel::Stereo); + recordGroup->addAction (new QAction(tr("Mono left"), recordGroup))->setData (SampleTrack::RecordingChannel::MonoLeft); + recordGroup->addAction (new QAction(tr("Mono right"), recordGroup))->setData (SampleTrack::RecordingChannel::MonoRight); + + recordGroup->setExclusive (true); + + for (auto *action : recordGroup->actions ()) { + action->setCheckable (true); + } + + // Check stereo by default. + recordGroup->actions ().first ()->setChecked (true); + + connect (recordGroup, SIGNAL(triggered(QAction*)), SLOT(onRecordChannelSelected(QAction*))); + + recordMenu->addActions (recordGroup->actions ()); + + QToolButton * recordTool = new QToolButton(this); + recordTool->setMenu(recordMenu); + recordTool->setToolTip (tr ("Select default channels to record.")); + recordTool->setPopupMode( QToolButton::InstantPopup ); + recordTool->setFixedWidth (17); + + m_toolBar->addWidget (recordTool); + } // Track actions DropToolBar *trackActionsToolBar = addDropToolBarToTop(tr("Track actions")); @@ -818,6 +853,13 @@ void SongEditorWindow::adjustUiAfterProjectLoad() m_editor->scrolled(0); } +void SongEditorWindow::onRecordChannelSelected(QAction *action) { + action->setChecked (true); + + // Set the recording channel according to the action's data. + m_globalRecordChannel = static_cast(action->data ().value()); +} + @@ -845,3 +887,8 @@ void SongEditorWindow::keyReleaseEvent( QKeyEvent *ke ) } } } + +SampleTrack::RecordingChannel SongEditorWindow::globalRecordChannel() const +{ + return m_globalRecordChannel; +} diff --git a/src/gui/widgets/EnvelopeAndLfoView.cpp b/src/gui/widgets/EnvelopeAndLfoView.cpp index cdad83d4fe7..64dcb4e267f 100644 --- a/src/gui/widgets/EnvelopeAndLfoView.cpp +++ b/src/gui/widgets/EnvelopeAndLfoView.cpp @@ -443,7 +443,6 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) } // userWaveSample() may be used, called out of loop for efficiency - m_params->m_userWave.dataReadLock(); float old_y = 0; for( int x = 0; x <= LFO_GRAPH_W; ++x ) { @@ -494,7 +493,6 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) graph_y_base + cur_y ) ); old_y = cur_y; } - m_params->m_userWave.dataUnlock(); p.setPen( QColor( 201, 201, 225 ) ); int ms_per_osc = static_cast( SECS_PER_LFO_OSCILLATION * @@ -515,7 +513,7 @@ void EnvelopeAndLfoView::lfoUserWaveChanged() if( m_params->m_lfoWaveModel.value() == EnvelopeAndLfoParameters::UserDefinedWave ) { - if( m_params->m_userWave.frames() <= 1 ) + if( m_params->m_userWaveInfo->frames <= 1 ) { TextFloat::displayMessage( tr( "Hint" ), tr( "Drag and drop a sample into this window." ), diff --git a/src/gui/widgets/Graph.cpp b/src/gui/widgets/Graph.cpp index 4710089dd1a..0a41ac1bdad 100644 --- a/src/gui/widgets/Graph.cpp +++ b/src/gui/widgets/Graph.cpp @@ -23,6 +23,8 @@ * */ +#include + #include #include #include @@ -582,21 +584,18 @@ void graphModel::setWaveToNoise() QString graphModel::setWaveToUser() { - SampleBuffer * sampleBuffer = new SampleBuffer; + auto sampleBuffer = std::unique_ptr{new SampleBuffer}; + QString fileName = sampleBuffer->openAndSetWaveformFile(); if( fileName.isEmpty() == false ) { - sampleBuffer->dataReadLock(); for( int i = 0; i < length(); i++ ) { m_samples[i] = sampleBuffer->userWaveSample( i / static_cast( length() ) ); } - sampleBuffer->dataUnlock(); } - sharedObject::unref( sampleBuffer ); - emit samplesChanged( 0, length() - 1 ); return fileName; }; diff --git a/src/tracks/AutomationTrack.cpp b/src/tracks/AutomationTrack.cpp index 11c919f0e42..5eadcb74d32 100644 --- a/src/tracks/AutomationTrack.cpp +++ b/src/tracks/AutomationTrack.cpp @@ -33,6 +33,7 @@ #include "TrackContainerView.h" #include "TrackLabelButton.h" +#include AutomationTrack::AutomationTrack( TrackContainer* tc, bool _hidden ) : Track( _hidden ? HiddenAutomationTrack : Track::AutomationTrack, tc ) @@ -57,7 +58,7 @@ TrackView * AutomationTrack::createView( TrackContainerView* tcv ) -TrackContentObject * AutomationTrack::createTCO( const MidiTime & ) +TrackContentObject * AutomationTrack::unsafeCreateTCO( const MidiTime & ) { return new AutomationPattern( this ); } @@ -140,4 +141,34 @@ void AutomationTrackView::dropEvent( QDropEvent * _de ) update(); } +void AutomationTrackView::updateTrackOperationsWidgetMenu(TrackOperationsWidget *trackOperations) { + TrackView::updateTrackOperationsWidgetMenu (trackOperations); + + auto toMenu = trackOperations->trackOps ()->menu (); + + toMenu->addAction( tr( "Turn all recording on" ), this, SLOT( recordingOn() ) ); + toMenu->addAction( tr( "Turn all recording off" ), this, SLOT( recordingOff() ) ); +} + +void AutomationTrackView::recordingOn() { + const Track::tcoVector & tcov = getTrack()->getTCOs(); + for( Track::tcoVector::const_iterator it = tcov.begin(); it != tcov.end(); ++it ) + { + AutomationPattern * ap = dynamic_cast( *it ); + if( ap ) { ap->setRecording( true ); } + } + update(); +} + + +void AutomationTrackView::recordingOff() { + const Track::tcoVector & tcov = getTrack()->getTCOs(); + for( Track::tcoVector::const_iterator it = tcov.begin(); it != tcov.end(); ++it ) + { + AutomationPattern * ap = dynamic_cast( *it ); + if( ap ) { ap->setRecording( false ); } + } + update(); +} + diff --git a/src/tracks/BBTrack.cpp b/src/tracks/BBTrack.cpp index 205a22087f8..aac6f44464a 100644 --- a/src/tracks/BBTrack.cpp +++ b/src/tracks/BBTrack.cpp @@ -474,7 +474,7 @@ TrackView * BBTrack::createView( TrackContainerView* tcv ) -TrackContentObject * BBTrack::createTCO( const MidiTime & _pos ) +TrackContentObject * BBTrack::unsafeCreateTCO(const MidiTime &_pos) { BBTCO * bbtco = new BBTCO( this ); if( s_lastTCOColor ) diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 6c95f3c9a23..da4e716f7e1 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -707,11 +707,8 @@ bool InstrumentTrack::play( const MidiTime & _start, const fpp_t _frames, } - - -TrackContentObject * InstrumentTrack::createTCO( const MidiTime & ) -{ - return new Pattern( this ); +TrackContentObject *InstrumentTrack::unsafeCreateTCO(const MidiTime &) { + return new Pattern(this); } @@ -1260,6 +1257,18 @@ QMenu * InstrumentTrackView::createFxMenu(QString title, QString newFxLabel) return fxMenu; } +void InstrumentTrackView::updateTrackOperationsWidgetMenu(TrackOperationsWidget *trackOperations) +{ + TrackView::updateTrackOperationsWidgetMenu (trackOperations); + + auto toMenu = trackOperations->trackOps ()->menu (); + QMenu *fxMenu = createFxMenu( tr( "FX %1: %2" ), tr( "Assign to new FX Channel" )); + toMenu->addMenu(fxMenu); + + toMenu->addSeparator(); + toMenu->addMenu( midiMenu() ); +} + diff --git a/src/tracks/Pattern.cpp b/src/tracks/Pattern.cpp index 32baf0b1484..c24a5dbf4b6 100644 --- a/src/tracks/Pattern.cpp +++ b/src/tracks/Pattern.cpp @@ -37,7 +37,6 @@ #include "PianoRoll.h" #include "RenameDialog.h" #include "SampleBuffer.h" -#include "AudioSampleRecorder.h" #include "BBTrackContainer.h" #include "StringPairDrag.h" #include "MainWindow.h" diff --git a/src/tracks/SampleTrack.cpp b/src/tracks/SampleTrack.cpp index 959d6389e8c..fdcefb68e73 100644 --- a/src/tracks/SampleTrack.cpp +++ b/src/tracks/SampleTrack.cpp @@ -1,4 +1,4 @@ -/* +/* * SampleTrack.cpp - implementation of class SampleTrack, a track which * provides arrangement of samples * @@ -52,14 +52,15 @@ #include "FxMixerView.h" #include "TabWidget.h" #include "TrackLabelButton.h" +#include "SampleBuffer.h" SampleTCO::SampleTCO( Track * _track ) : TrackContentObject( _track ), m_sampleBuffer( new SampleBuffer ), + m_sampleBufferInfo(m_sampleBuffer->createUpdatingValue(this)), m_isPlaying( false ) { saveJournallingState( false ); - setSampleFile( "" ); restoreJournallingState(); // we need to receive bpm-change-events, because then we have to @@ -69,25 +70,8 @@ SampleTCO::SampleTCO( Track * _track ) : connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int,int ) ), this, SLOT( updateLength() ) ); - //care about positionmarker - TimeLineWidget * timeLine = Engine::getSong()->getPlayPos( Engine::getSong()->Mode_PlaySong ).m_timeLine; - if( timeLine ) - { - connect( timeLine, SIGNAL( positionMarkerMoved() ), this, SLOT( playbackPositionChanged() ) ); - } - //playbutton clicked or space key / on Export Song set isPlaying to false - connect( Engine::getSong(), SIGNAL( playbackStateChanged() ), - this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); - //care about loops - connect( Engine::getSong(), SIGNAL( updateSampleTracks() ), - this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); - //care about mute TCOs - connect( this, SIGNAL( dataChanged() ), this, SLOT( playbackPositionChanged() ) ); - //care about mute track - connect( getTrack()->getMutedModel(), SIGNAL( dataChanged() ), - this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); - //care about TCO position - connect( this, SIGNAL( positionChanged() ), this, SLOT( updateTrackTcos() ) ); + connect(m_sampleBuffer.get(), + &SampleBuffer::sampleUpdated, this, &SampleTCO::onSampleBufferChanged); switch( getTrack()->trackContainer()->type() ) { @@ -101,7 +85,12 @@ SampleTCO::SampleTCO( Track * _track ) : setAutoResize( false ); break; } - updateTrackTcos(); + + //care about TCO position + connect(this, SIGNAL(positionChanged()), getTrack(), SLOT(updateTcos())); + + // Care about finishing setAudioFile. + connect(&m_loadingWatcher, &Watcher::finished, this, &SampleTCO::onFileLoadingFinished); } @@ -114,7 +103,6 @@ SampleTCO::~SampleTCO() { sampletrack->updateTcos(); } - sharedObject::unref( m_sampleBuffer ); } @@ -126,46 +114,32 @@ void SampleTCO::changeLength( const MidiTime & _length ) } - - -const QString & SampleTCO::sampleFile() const -{ - return m_sampleBuffer->audioFile(); +const QString &SampleTCO::sampleFile() const { + return m_sampleBufferInfo->audioFile; } +void SampleTCO::setSampleFile(const QString &_sf) { + // We're in the process of loading another file. + if (m_loadingWatcher.isRunning()) + return; -void SampleTCO::setSampleBuffer( SampleBuffer* sb ) -{ - sharedObject::unref( m_sampleBuffer ); - m_sampleBuffer = sb; - updateLength(); - - emit sampleChanged(); -} - + auto sampleBuffer = m_sampleBuffer; + // Load the file. + m_loadingWatcher.setFuture(runAsync([sampleBuffer, _sf] { + sampleBuffer->setAudioFile(_sf); + return sampleBuffer->createInfo(); + })); -void SampleTCO::setSampleFile( const QString & _sf ) -{ - int length; - if ( _sf.isEmpty() ) - { //When creating an empty sample pattern make it a bar long - float nom = Engine::getSong()->getTimeSigModel().getNumerator(); - float den = Engine::getSong()->getTimeSigModel().getDenominator(); - length = DefaultTicksPerTact * ( nom / den ); - } - else - { //Otherwise set it to the sample's length - m_sampleBuffer->setAudioFile( _sf ); - length = sampleLength(); - } - changeLength(length); + // onFileLoadingFinished will run when the future has been finished. - setStartTimeOffset( 0 ); + // Already has been has been called sampleChanged from m_sampleBuffer. +} - emit sampleChanged(); - emit playbackPositionChanged(); +void SampleTCO::onFileLoadingFinished() { + setStartTimeOffset(0); + changeLength((int) (m_loadingWatcher.future().result().frames / Engine::framesPerTick())); } @@ -177,77 +151,34 @@ void SampleTCO::toggleRecord() emit dataChanged(); } - - - -void SampleTCO::playbackPositionChanged() -{ - Engine::mixer()->removePlayHandlesOfTypes( getTrack(), PlayHandle::TypeSamplePlayHandle ); - SampleTrack * st = dynamic_cast( getTrack() ); - st->setPlayingTcos( false ); -} - - - - -void SampleTCO::updateTrackTcos() -{ - SampleTrack * sampletrack = dynamic_cast( getTrack() ); - if( sampletrack) - { - sampletrack->updateTcos(); - } -} - - - - -bool SampleTCO::isPlaying() const -{ +bool SampleTCO::isPlaying() const { return m_isPlaying; } - - -void SampleTCO::setIsPlaying(bool isPlaying) -{ +void SampleTCO::setIsPlaying(bool isPlaying) { + auto guard = Engine::mixer()->requestChangesGuard(); m_isPlaying = isPlaying; } - - - -void SampleTCO::updateLength() +bool SampleTCO::isEmpty() const { - emit sampleChanged(); + return (sampleLength () == 0); } - - -MidiTime SampleTCO::sampleLength() const -{ - return (int)( m_sampleBuffer->frames() / Engine::framesPerTick() ); +void SampleTCO::updateLength() { + emit sampleChanged(SampleBuffer::UpdateType::Clear); } - - -void SampleTCO::setSampleStartFrame(f_cnt_t startFrame) -{ - m_sampleBuffer->setStartFrame( startFrame ); +MidiTime SampleTCO::sampleLength() const { + return (int) (m_sampleBufferInfo->frames / Engine::framesPerTick(m_sampleBufferInfo->sampleRate)); } -void SampleTCO::setSamplePlayLength(f_cnt_t length) -{ - m_sampleBuffer->setEndFrame( length ); -} - - void SampleTCO::saveSettings( QDomDocument & _doc, QDomElement & _this ) @@ -262,13 +193,11 @@ void SampleTCO::saveSettings( QDomDocument & _doc, QDomElement & _this ) } _this.setAttribute( "len", length() ); _this.setAttribute( "muted", isMuted() ); - _this.setAttribute( "src", sampleFile() ); _this.setAttribute( "off", startTimeOffset() ); - if( sampleFile() == "" ) - { - QString s; - _this.setAttribute( "data", m_sampleBuffer->toBase64( s ) ); - } + + m_sampleBuffer->saveState (_doc, _this); + + _this.setAttribute ("is_record", isRecord ()); // TODO: start- and end-frame } @@ -281,14 +210,35 @@ void SampleTCO::loadSettings( const QDomElement & _this ) { movePosition( _this.attribute( "pos" ).toInt() ); } - setSampleFile( _this.attribute( "src" ) ); - if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) ) - { - m_sampleBuffer->loadFromBase64( _this.attribute( "data" ) ); + + // Should not be happening after 1.3. + if (_this.hasAttribute ("src")) { + auto audioFile = _this.attribute ("src"); + + if (! audioFile.isEmpty ()) { + setSampleFile( _this.attribute( "src" ) ); + } + } + + if (sampleFile () == QString()) { + // Data without any other info. + // Should not be happening after 1.3. + if (_this.hasAttribute ("data") && _this.attribute ("data") != QString()) { + qWarning("Using default sampleRate. That could lead to invalid values1"); + *m_sampleBuffer = SampleBuffer(_this.attribute ("data"), + Engine::mixer ()->baseSampleRate ()); + } else { + m_sampleBuffer->restoreState (_this.firstChildElement (m_sampleBuffer->nodeName ())); + } } + changeLength( _this.attribute( "len" ).toInt() ); setMuted( _this.attribute( "muted" ).toInt() ); setStartTimeOffset( _this.attribute( "off" ).toInt() ); + + if (_this.hasAttribute ("is_record")) { + setRecord (_this.attribute ("is_record").toInt ()); + } } @@ -299,37 +249,40 @@ TrackContentObjectView * SampleTCO::createView( TrackView * _tv ) return new SampleTCOView( this, _tv ); } - +void SampleTCO::onSampleBufferChanged(int updateType) { + emit sampleChanged(updateType); +} SampleTCOView::SampleTCOView( SampleTCO * _tco, TrackView * _tv ) : TrackContentObjectView( _tco, _tv ), - m_tco( _tco ), - m_paintPixmap() + m_tco( _tco ) { // update UI and tooltip - updateSample(); + updateSample(SampleBuffer::UpdateType::Clear); + + // Update the view on each visualization. + QObject::connect(&m_visualizationWatcher, &Watcher::finished, this, &SampleTCOView::update); // track future changes of SampleTCO - connect( m_tco, SIGNAL( sampleChanged() ), - this, SLOT( updateSample() ) ); + connect(m_tco, &SampleTCO::sampleChanged, + this, &SampleTCOView::updateSample); setStyle( QApplication::style() ); } -void SampleTCOView::updateSample() -{ - update(); +void SampleTCOView::updateSample(int updateType) { + updateVisualizer(mutedColor(), + static_cast(updateType)); + // set tooltip to filename so that user can see what sample this // sample-tco contains - ToolTip::add( this, ( m_tco->m_sampleBuffer->audioFile() != "" ) ? - m_tco->m_sampleBuffer->audioFile() : + ToolTip::add( this, ( m_tco->m_sampleBufferInfo->audioFile != "" ) ? + m_tco->m_sampleBufferInfo->audioFile : tr( "Double-click to open sample" ) ); + setNeedsUpdate (true); } - - - void SampleTCOView::contextMenuEvent( QContextMenuEvent * _cme ) { if( _cme->modifiers() ) @@ -355,9 +308,9 @@ void SampleTCOView::contextMenuEvent( QContextMenuEvent * _cme ) contextMenu.addAction( embed::getIconPixmap( "muted" ), tr( "Mute/unmute (<%1> + middle click)" ).arg(UI_CTRL_KEY), m_tco, SLOT( toggleMute() ) ); - /*contextMenu.addAction( embed::getIconPixmap( "record" ), - tr( "Set/clear record" ), - m_tco, SLOT( toggleRecord() ) );*/ + contextMenu.addAction( embed::getIconPixmap( "record" ), + tr( "Set/clear record (Shift + Ctrl + left click)" ), + m_tco, SLOT( toggleRecord() ) ); constructContextMenu( &contextMenu ); contextMenu.exec( QCursor::pos() ); @@ -387,15 +340,6 @@ void SampleTCOView::dropEvent( QDropEvent * _de ) m_tco->setSampleFile( StringPairDrag::decodeValue( _de ) ); _de->accept(); } - else if( StringPairDrag::decodeKey( _de ) == "sampledata" ) - { - m_tco->m_sampleBuffer->loadFromBase64( - StringPairDrag::decodeValue( _de ) ); - m_tco->updateLength(); - update(); - _de->accept(); - Engine::getSong()->setModified(); - } else { TrackContentObjectView::dropEvent( _de ); @@ -420,7 +364,7 @@ void SampleTCOView::mousePressEvent( QMouseEvent * _me ) SampleTCO * sTco = dynamic_cast( getTrackContentObject() ); if( sTco ) { - sTco->updateTrackTcos(); + static_cast(sTco->getTrack ())->updateTcos (); } } TrackContentObjectView::mousePressEvent( _me ); @@ -437,7 +381,7 @@ void SampleTCOView::mouseReleaseEvent(QMouseEvent *_me) SampleTCO * sTco = dynamic_cast( getTrackContentObject() ); if( sTco ) { - sTco->playbackPositionChanged(); + static_cast(sTco->getTrack ())->playbackPositionChanged(); } } TrackContentObjectView::mouseReleaseEvent( _me ); @@ -449,15 +393,7 @@ void SampleTCOView::mouseReleaseEvent(QMouseEvent *_me) void SampleTCOView::mouseDoubleClickEvent( QMouseEvent * ) { QString af = m_tco->m_sampleBuffer->openAudioFile(); - - if ( af.isEmpty() ) {} //Don't do anything if no file is loaded - else if ( af == m_tco->m_sampleBuffer->audioFile() ) - { //Instead of reloading the existing file, just reset the size - int length = (int) ( m_tco->m_sampleBuffer->frames() / Engine::framesPerTick() ); - m_tco->changeLength(length); - } - else - { //Otherwise load the new file as ususal + if(af != "" && af != m_tco->m_sampleBufferInfo->audioFile) { m_tco->setSampleFile( af ); Engine::getSong()->setModified(); } @@ -468,30 +404,27 @@ void SampleTCOView::mouseDoubleClickEvent( QMouseEvent * ) void SampleTCOView::paintEvent( QPaintEvent * pe ) { - QPainter painter( this ); - - if( !needsUpdate() ) - { - painter.drawPixmap( 0, 0, m_paintPixmap ); + if (! m_sampleBufferVisualizerMutex->tryLock()) { + update(); return; } - setNeedsUpdate( false ); - - if (m_paintPixmap.isNull() || m_paintPixmap.size() != size()) - { - m_paintPixmap = QPixmap(size()); - } + QPainter p( this ); - QPainter p( &m_paintPixmap ); + setNeedsUpdate( false ); QLinearGradient lingrad( 0, 0, 0, height() ); QColor c; bool muted = m_tco->getTrack()->isMuted() || m_tco->isMuted(); - // state: selected, muted, normal - c = isSelected() ? selectedColor() : ( muted ? mutedBackgroundColor() - : painter.background().color() ); + if (isSelected ()) + c = selectedColor (); + else if (muted) + c = mutedBackgroundColor (); + else if (m_tco->isRecord ()) + c = recordingBackgroundColor (); + else + c = p.background ().color (); lingrad.setColorAt( 1, c.darker( 300 ) ); lingrad.setColorAt( 0, c ); @@ -508,24 +441,14 @@ void SampleTCOView::paintEvent( QPaintEvent * pe ) p.fillRect( rect(), c ); } - p.setPen( !muted ? painter.pen().brush().color() : mutedColor() ); +// p.setPen(!muted ? p.pen().brush().color() : mutedColor()); - const int spacing = TCO_BORDER_WIDTH + 1; - const float ppt = fixedTCOs() ? - ( parentWidget()->width() - 2 * TCO_BORDER_WIDTH ) - / (float) m_tco->length().getTact() : - pixelsPerTact(); - - float nom = Engine::getSong()->getTimeSigModel().getNumerator(); - float den = Engine::getSong()->getTimeSigModel().getDenominator(); - float ticksPerTact = DefaultTicksPerTact * nom / den; - - float offset = m_tco->startTimeOffset() / ticksPerTact * pixelsPerTact(); - QRect r = QRect( TCO_BORDER_WIDTH + offset, spacing, - qMax( static_cast( m_tco->sampleLength() * ppt / ticksPerTact ), 1 ), rect().bottom() - 2 * spacing ); - m_tco->m_sampleBuffer->visualize( p, r, pe->rect() ); - - QFileInfo fileInfo(m_tco->m_sampleBuffer->audioFile()); + { + m_sampleBufferVisualizer->draw(p); + m_sampleBufferVisualizerMutex->unlock(); + } + + QFileInfo fileInfo(m_tco->m_sampleBufferInfo->audioFile); QString filename = fileInfo.fileName(); paintTextLabel(filename, p); @@ -550,45 +473,92 @@ void SampleTCOView::paintEvent( QPaintEvent * pe ) embed::getIconPixmap( "muted", size, size ) ); } - // recording sample tracks is not possible at the moment - - /* if( m_tco->isRecord() ) - { - p.setFont( pointSize<7>( p.font() ) ); - - p.setPen( textShadowColor() ); - p.drawText( 10, p.fontMetrics().height()+1, "Rec" ); - p.setPen( textColor() ); - p.drawText( 9, p.fontMetrics().height(), "Rec" ); - - p.setBrush( QBrush( textColor() ) ); - p.drawEllipse( 4, 5, 4, 4 ); - }*/ - p.end(); - - painter.drawPixmap( 0, 0, m_paintPixmap ); } +void SampleTCOView::updateVisualizer(QPen p, SampleBufferVisualizer::Operation operation) { + auto timeSig = TimeSig(Engine::getSong()->getTimeSigModel()); + auto realTicksPerDefaultTicks = float(float(MidiTime::ticksPerTact(timeSig) / MidiTime::ticksPerTact())); + auto normalizedPixelsPerTact = pixelsPerTact() * realTicksPerDefaultTicks; + auto normalizedFramesPerTick = + Engine::framesPerTick(m_tco->m_sampleBufferInfo->sampleRate) * realTicksPerDefaultTicks; + const int spacing = TCO_BORDER_WIDTH + 1; - + QMargins margins(spacing, + TCO_BORDER_WIDTH - 1, + spacing, + TCO_BORDER_WIDTH); + + // Prepare values for the lambda. + auto rectMinusMargins = rect() - margins; + auto startTimeOffset = m_tco->startTimeOffset(); + auto sampleLength = m_tco->sampleLength(); + + // Prepare shared_ptr-s to be captured by the lambda. + auto sampleBuffer = m_tco->sampleBuffer(); + auto visualizer = m_sampleBufferVisualizer; + auto visualizerMutex = m_sampleBufferVisualizerMutex; + + // NOTE: we will use only local variables and won't + // touch `this`; as far as we know, this could be + // dangling. + m_visualizationWatcher.setFuture(runAsync([=] { + QMutexLocker locker{visualizerMutex.get()}; + visualizer->update( + *sampleBuffer, + startTimeOffset, + sampleLength, + rectMinusMargins, + normalizedPixelsPerTact, + f_cnt_t(normalizedFramesPerTick), + p, + operation + ); + })); +} SampleTrack::SampleTrack( TrackContainer* tc ) : Track( Track::SampleTrack, tc ), + m_recordingChannelModel(RecordingChannel::None, + RecordingChannel::None, + RecordingChannel::Stereo, + this, + tr ("Record channel")), m_volumeModel( DefaultVolume, MinVolume, MaxVolume, 0.1f, this, - tr( "Volume" ) ), + tr( "Volume" ) ), m_panningModel( DefaultPanning, PanningLeft, PanningRight, 0.1f, this, tr( "Panning" ) ), m_effectChannelModel( 0, 0, 0, this, tr( "FX channel" ) ), m_audioPort( tr( "Sample track" ), true, &m_volumeModel, &m_panningModel, &m_mutedModel ) + { setName( tr( "Sample track" ) ); m_panningModel.setCenterValue( DefaultPanning ); m_effectChannelModel.setRange( 0, Engine::fxMixer()->numChannels()-1, 1); connect( &m_effectChannelModel, SIGNAL( dataChanged() ), this, SLOT( updateEffectChannel() ) ); + connect (Engine::getSong (), SIGNAL(beforeRecordOn(MidiTime)), this, SLOT(beforeRecordOn(MidiTime))); + + + //care about positionmarker + TimeLineWidget * timeLine = Engine::getSong()->getPlayPos( Engine::getSong()->Mode_PlaySong ).m_timeLine; + if( timeLine ) + { + connect( timeLine, SIGNAL( positionMarkerMoved() ), this, SLOT( playbackPositionChanged() ) ); + } + //playbutton clicked or space key / on Export Song set isPlaying to false + connect( Engine::getSong(), SIGNAL( playbackStateChanged() ), + this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); + //care about loops + connect( Engine::getSong(), SIGNAL( updateSampleTracks() ), + this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); + //care about mute TCOs + connect( this, SIGNAL( dataChanged() ), this, SLOT( playbackPositionChanged() ) ); + //care about mute track + connect( getMutedModel(), SIGNAL( dataChanged() ), + this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); } @@ -605,7 +575,8 @@ SampleTrack::~SampleTrack() bool SampleTrack::play( const MidiTime & _start, const fpp_t _frames, const f_cnt_t _offset, int _tco_num ) { - m_audioPort.effects()->startRunning(); + if (m_audioPort.effects()) + m_audioPort.effects()->startRunning(); bool played_a_note = false; // will be return variable tcoVector tcos; @@ -628,24 +599,40 @@ bool SampleTrack::play( const MidiTime & _start, const fpp_t _frames, { TrackContentObject * tco = getTCO( i ); SampleTCO * sTco = dynamic_cast( tco ); - float framesPerTick = Engine::framesPerTick(); + + // If this is an automatically created record track, resize it to the current + // position. + if (sTco->isRecord () && !sTco->isMuted () && sTco->getAutoResize ()) { + sTco->changeLength (_start - sTco->startPosition()); + } + if( _start >= sTco->startPosition() && _start < sTco->endPosition() ) { - if( sTco->isPlaying() == false && _start > sTco->startPosition() + sTco->startTimeOffset() ) + if( sTco->isPlaying() == false && (_start >= (sTco->startPosition() + sTco->startTimeOffset()) + || sTco->isRecord ()) ) { - f_cnt_t sampleStart = framesPerTick * ( _start - sTco->startPosition() - sTco->startTimeOffset() ); - f_cnt_t tcoFrameLength = framesPerTick * ( sTco->endPosition() - sTco->startPosition() - sTco->startTimeOffset() ); - f_cnt_t sampleBufferLength = sTco->sampleBuffer()->frames(); + auto sampleBufferInfo = sTco->sampleBuffer()->createInfo(); + + auto bufferFramesPerTick = Engine::framesPerTick(sampleBufferInfo.sampleRate); + f_cnt_t sampleStart = + bufferFramesPerTick * (_start - sTco->startPosition() - sTco->startTimeOffset()); + + f_cnt_t tcoFrameLength = bufferFramesPerTick * + (sTco->endPosition() - sTco->startPosition() - sTco->startTimeOffset()); + + f_cnt_t sampleBufferLength = sampleBufferInfo.frames; //if the Tco smaller than the sample length we play only until Tco end //else we play the sample to the end but nothing more - f_cnt_t samplePlayLength = tcoFrameLength > sampleBufferLength ? sampleBufferLength : tcoFrameLength; + f_cnt_t samplePlayLength = + tcoFrameLength > sampleBufferLength ? sampleBufferLength : tcoFrameLength; //we only play within the sampleBuffer limits - if( sampleStart < sampleBufferLength ) - { - sTco->setSampleStartFrame( sampleStart ); - sTco->setSamplePlayLength( samplePlayLength ); - tcos.push_back( sTco ); - sTco->setIsPlaying( true ); + // anyway, "play" (record) this TCO if is recording. + if (sampleStart < sampleBufferLength || sTco->isRecord()) { + auto sampleBuffer = sTco->sampleBuffer(); + sampleBuffer->setStartFrame(sampleStart);; + sampleBuffer->setEndFrame(samplePlayLength); + tcos.push_back(sTco); + sTco->setIsPlaying(true); } } } @@ -668,7 +655,7 @@ bool SampleTrack::play( const MidiTime & _start, const fpp_t _frames, { return played_a_note; } - SampleRecordHandle* smpHandle = new SampleRecordHandle( st ); + SampleRecordHandle* smpHandle = new SampleRecordHandle( st , _start - st->startPosition ()); handle = smpHandle; } else @@ -697,11 +684,8 @@ TrackView * SampleTrack::createView( TrackContainerView* tcv ) } - - -TrackContentObject * SampleTrack::createTCO( const MidiTime & ) -{ - return new SampleTCO( this ); +TrackContentObject *SampleTrack::unsafeCreateTCO(const MidiTime &) { + return new SampleTCO(this); } @@ -717,6 +701,8 @@ void SampleTrack::saveTrackSpecificSettings( QDomDocument & _doc, m_volumeModel.saveSettings( _doc, _this, "vol" ); m_panningModel.saveSettings( _doc, _this, "pan" ); m_effectChannelModel.saveSettings( _doc, _this, "fxch" ); + m_recordModel.saveSettings(_doc, _this, "record"); + m_recordingChannelModel.saveSettings (_doc, _this, "record_channel"); } @@ -737,10 +723,13 @@ void SampleTrack::loadTrackSpecificSettings( const QDomElement & _this ) } node = node.nextSibling(); } + m_volumeModel.loadSettings( _this, "vol" ); m_panningModel.loadSettings( _this, "pan" ); m_effectChannelModel.setRange( 0, Engine::fxMixer()->numChannels() - 1 ); m_effectChannelModel.loadSettings( _this, "fxch" ); + m_recordModel.loadSettings (_this, "record"); + m_recordingChannelModel.loadSettings (_this, "record_channel"); } @@ -765,8 +754,65 @@ void SampleTrack::setPlayingTcos( bool isPlaying ) } } +void SampleTrack::beforeRecordOn(MidiTime time) +{ + if (isRecord ()) { + bool isRecordTCOExist = false; + + for(auto &track : getTCOs ()) { + auto sampleTCO = static_cast(track); + + if (sampleTCO->isRecord() && !sampleTCO->isMuted()) { + isRecordTCOExist = true; + } + } + if (! isRecordTCOExist) { + // TODO: instead of this strange thing, move this code to SampleTrack::play. + Engine::mixer()->requestChangeInModel(); + auto fallbackRecordTCO = static_cast(createTCO (0)); + + fallbackRecordTCO->setRecord (true); + fallbackRecordTCO->movePosition (time); +// fallbackRecordTCO->setSamplePlayLength (Engine::framesPerTick()); + fallbackRecordTCO->changeLength (1); + fallbackRecordTCO->sampleBuffer()->setEndFrame (Engine::framesPerTick()); + fallbackRecordTCO->setIsPlaying (false); + + fallbackRecordTCO->setAutoResize (true); + + Engine::mixer()->doneChangeInModel(); + + } + } +} + +void SampleTrack::toggleRecord() { + setRecord (! isRecord ()); +} + +void SampleTrack::playbackPositionChanged() +{ + Engine::mixer()->removePlayHandlesOfTypes( this, PlayHandle::TypeSamplePlayHandle ); + + setPlayingTcos( false ); +} + +SampleTrack::RecordingChannel SampleTrack::recordingChannel() const{ + // If we had defined a recording channel for this track, use + // it. Otherwise, use the global setting. + if (m_recordingChannelModel.value () != static_cast(RecordingChannel::None)) { + return static_cast(m_recordingChannelModel.value ()); + } else { + return gui->songEditor ()->globalRecordChannel (); + } +} + +void SampleTrack::setRecordingChannel(const RecordingChannel &recordingChannel) +{ + m_recordingChannelModel.setValue (recordingChannel); +} void SampleTrack::updateEffectChannel() { @@ -870,6 +916,39 @@ QMenu * SampleTrackView::createFxMenu(QString title, QString newFxLabel) return fxMenu; } +void SampleTrackView::updateTrackOperationsWidgetMenu(TrackOperationsWidget *trackOperations) { + TrackView::updateTrackOperationsWidgetMenu (trackOperations); + + SampleTrack * st = castModel(); + auto toMenu = trackOperations->trackOps ()->menu (); + + QMenu *recordMenu = toMenu->addMenu (tr ("Set record channel")); + auto *recordChannels = new QActionGroup(recordMenu); + + recordChannels->setExclusive (true); + + recordChannels->addAction(tr( "Stereo" ))->setData (SampleTrack::RecordingChannel::Stereo); + recordChannels->addAction(tr( "Mono left" ))->setData (SampleTrack::RecordingChannel::MonoLeft); + recordChannels->addAction(tr( "Mono right" ))->setData (SampleTrack::RecordingChannel::MonoRight); + + for (auto *action : recordChannels->actions ()) { + action->setCheckable (true); + + if (action->data ().value() == st->m_recordingChannelModel.value ()) + { + action->setChecked (true); + } + } + + recordMenu->addActions (recordChannels->actions ()); + + connect (recordChannels, SIGNAL(triggered(QAction*)), SLOT(onRecordActionSelected(QAction*))); + + auto recordAction = toMenu->addAction( tr( "Toggle record" ), st, SLOT( toggleRecord() ) ); + recordAction->setCheckable (true); + recordAction->setChecked (st->isRecord ()); +} + @@ -1141,3 +1220,19 @@ void SampleTrackWindow::loadSettings(const QDomElement& element) } } + +void SampleTrackView::onRecordActionSelected(QAction *action) { + SampleTrack * st = castModel(); + auto selectedRecordingChannel = static_cast(action->data ().value()); + + // If we've selected the current recording channel again, we should undo it. + if (selectedRecordingChannel == static_cast(st->m_recordingChannelModel.value ())) { + st->setRecordingChannel (SampleTrack::RecordingChannel::None); + action->setChecked (false); + } else { + st->setRecordingChannel (selectedRecordingChannel); + action->setChecked (true); + } + + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ddebe116c6e..d156d13ad05 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,8 @@ ADD_EXECUTABLE(tests src/core/ProjectVersionTest.cpp src/core/RelativePathsTest.cpp + src/core/UpdatingValueTest.cpp + src/tracks/AutomationTrackTest.cpp ) TARGET_COMPILE_DEFINITIONS(tests diff --git a/tests/src/core/UpdatingValueTest.cpp b/tests/src/core/UpdatingValueTest.cpp new file mode 100644 index 00000000000..e0457748224 --- /dev/null +++ b/tests/src/core/UpdatingValueTest.cpp @@ -0,0 +1,109 @@ +#include "QTestSuite.h" + +#include + +#include "UpdatingValue.h" +#include "Threading.h" +typedef UpdatingValue Value; + +class _QThreadUpdatingValueTest : public QObject +{ + Q_OBJECT + +public: + _QThreadUpdatingValueTest(Value::Notifier *notifier) + : notifier{notifier} + { + } + +public slots: + void emitNotifier() { + notifier->onValueUpdated(1); + } + +private: + Value::Notifier *notifier; +}; + +class UpdatingValueTest : QTestSuite +{ + Q_OBJECT + + + + std::unique_ptr createNotifierOnExternalThread() { + auto future = runAsync([] { + return new Value::Notifier(); + }); + + future.waitForFinished(); + + std::unique_ptr ptr; + ptr.reset(future.result()); + + return ptr; + } +private slots: + + void SameThreadUpdatingValueTest() { + Value::Notifier notifier; + Value value{notifier, 0, this}; + Value value2{notifier, 0, this}; + + QCOMPARE(*value, 0); + QCOMPARE(*value2, 0); + + notifier.onValueUpdated(1); + QCOMPARE(*value, 1); + QCOMPARE(*value2, 1); + } + + void ExternalThreadUpdatingValueTest() { + auto notifier = createNotifierOnExternalThread(); + Value value{*notifier, 0, this}; + Value value2{*notifier, 0, this}; + + runAsync([¬ifier] { + notifier->onValueUpdated(1); + }).waitForFinished(); + + QCOMPARE(*value, 1); + QCOMPARE(*value2, 1); + } + void QThreadUpdatingValueTest() { + // First of all, create the notifier on another thread. + auto notifier = createNotifierOnExternalThread(); + + Value value{*notifier, 0, this}; + Value value2{*notifier, 0, this}; + + QThread *thread = new QThread; + + QObject *emittingThreadObject = new _QThreadUpdatingValueTest(notifier.get()); + emittingThreadObject->moveToThread(thread); + + QSignalSpy valueChangedSpy{notifier.get(), &internal::UpdatingValueNotifier_Untyped::rawOnValueUpdated}; + + thread->start(); + + // Invoke emitNotifier on the objects thread. + QMetaObject::invokeMethod(emittingThreadObject, + "emitNotifier"); + + + // Wait for the signal to come. + QVERIFY(valueChangedSpy.wait()); + + thread->quit(); + thread->wait(); + + QCOMPARE(*value, 1); + QCOMPARE(*value2, 1); + + delete thread; + delete emittingThreadObject; + + } +} updatingValueTest; + +#include "UpdatingValueTest.moc"