diff --git a/include/AudioEngine.h b/include/AudioEngine.h index f80c860d819..6fc59bfd210 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -169,6 +169,8 @@ class LMMS_EXPORT AudioEngine : public QObject return m_audioDev; } + bool captureDeviceAvailable() const; + // audio-port-stuff inline void addAudioPort(AudioPort * port) @@ -343,6 +345,11 @@ class LMMS_EXPORT AudioEngine : public QObject AudioDevice * tryAudioDevices(); MidiClient * tryMidiClients(); + const AudioDevice* audioDev() const + { + return m_audioDev; + } + void renderStageNoteSetup(); void renderStageInstruments(); void renderStageEffects(); diff --git a/include/SampleClipView.h b/include/SampleClipView.h index 4ff218fb0ef..0faf8c2754b 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -60,6 +60,7 @@ public slots: void mouseDoubleClickEvent( QMouseEvent * ) override; void paintEvent( QPaintEvent * ) override; + bool recordingCapabilitiesAvailable() const; private: SampleClip * m_clip; diff --git a/include/SampleRecordHandle.h b/include/SampleRecordHandle.h index b650c67609f..6f67625b99e 100644 --- a/include/SampleRecordHandle.h +++ b/include/SampleRecordHandle.h @@ -45,7 +45,7 @@ class Track; class SampleRecordHandle : public PlayHandle { public: - SampleRecordHandle( SampleClip* clip ); + SampleRecordHandle(SampleClip* clip, TimePos startRecordTimeOffset); ~SampleRecordHandle() override; void play( SampleFrame* _working_buffer ) override; @@ -70,6 +70,9 @@ class SampleRecordHandle : public PlayHandle PatternTrack* m_patternTrack; SampleClip * m_clip; + // The offset from the start of m_track that the record has + // started from. + TimePos m_startRecordTimeOffset; } ; diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 42e7079b016..e4d02c2176b 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -327,7 +327,7 @@ void AudioEngine::renderStageNoteSetup() if( it != m_playHandles.end() ) { - ( *it )->audioPort()->removePlayHandle( ( *it ) ); + if ((*it)->audioPort()) { (*it)->audioPort()->removePlayHandle(*it); } if( ( *it )->type() == PlayHandle::Type::NotePlayHandle ) { NotePlayHandleManager::release( (NotePlayHandle*) *it ); @@ -392,7 +392,7 @@ void AudioEngine::renderStageEffects() } if( ( *it )->isFinished() ) { - ( *it )->audioPort()->removePlayHandle( ( *it ) ); + if ((*it)->audioPort()) { (*it)->audioPort()->removePlayHandle(*it); } if( ( *it )->type() == PlayHandle::Type::NotePlayHandle ) { NotePlayHandleManager::release( (NotePlayHandle*) *it ); @@ -633,6 +633,10 @@ void AudioEngine::restoreAudioDevice() } +bool AudioEngine::captureDeviceAvailable() const +{ + return audioDev()->supportsCapture(); +} void AudioEngine::removeAudioPort(AudioPort * port) @@ -656,7 +660,7 @@ bool AudioEngine::addPlayHandle( PlayHandle* handle ) if (handle->type() == PlayHandle::Type::InstrumentPlayHandle || !criticalXRuns()) { m_newPlayHandles.push( handle ); - handle->audioPort()->addPlayHandle( handle ); + if (handle->audioPort()) { handle->audioPort()->addPlayHandle(handle); } return true; } @@ -677,7 +681,7 @@ void AudioEngine::removePlayHandle(PlayHandle * ph) // which were created in a thread different than the audio engine thread 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 @@ -736,7 +740,7 @@ void AudioEngine::removePlayHandlesOfTypes(Track * track, PlayHandle::Types type { if ((*it)->isFromTrack(track) && ((*it)->type() & types)) { - ( *it )->audioPort()->removePlayHandle( ( *it ) ); + if ((*it)->audioPort()) { (*it)->audioPort()->removePlayHandle(*it); } if( ( *it )->type() == PlayHandle::Type::NotePlayHandle ) { NotePlayHandleManager::release( (NotePlayHandle*) *it ); diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 3e7d6d8b691..85946461fdc 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -467,7 +467,17 @@ bool DataFile::copyResources(const QString& resourcesDir) { // Get absolute path to resource bool error; - QString resPath = PathUtil::toAbsolute(el.attribute(*res), &error); + auto attribute = el.attribute(*res); + + // Skip empty resources. In some cases, there might be an + // alternative way of actually representing the data (e.g. data element for SampleTCO) + if (attribute.isEmpty()) + { + ++res; + continue; + } + + QString resPath = PathUtil::toAbsolute(attribute, &error); // If we are running without the project loaded (from CLI), "local:" base // prefixes aren't converted, so we need to convert it ourselves if (error) diff --git a/src/core/PlayHandle.cpp b/src/core/PlayHandle.cpp index 134fcd31100..af485dd0c5d 100644 --- a/src/core/PlayHandle.cpp +++ b/src/core/PlayHandle.cpp @@ -39,7 +39,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/SampleRecordHandle.cpp b/src/core/SampleRecordHandle.cpp index f7003f3beff..c378d467e87 100644 --- a/src/core/SampleRecordHandle.cpp +++ b/src/core/SampleRecordHandle.cpp @@ -36,13 +36,14 @@ namespace lmms { -SampleRecordHandle::SampleRecordHandle( SampleClip* clip ) : - PlayHandle( Type::SamplePlayHandle ), +SampleRecordHandle::SampleRecordHandle(SampleClip* clip, TimePos startRecordTimeOffset) : + PlayHandle(Type::SamplePlayHandle), m_framesRecorded( 0 ), m_minLength( clip->length() ), m_track( clip->getTrack() ), m_patternTrack( nullptr ), - m_clip( clip ) + m_clip(clip), + m_startRecordTimeOffset{startRecordTimeOffset} { } @@ -53,6 +54,8 @@ SampleRecordHandle::~SampleRecordHandle() { if (!m_buffers.empty()) { m_clip->setSampleBuffer(createSampleBuffer()); } + m_clip->setStartTimeOffset(m_startRecordTimeOffset); + while( !m_buffers.empty() ) { delete[] m_buffers.front().first; diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index 30f09caa860..1d4d9b78a71 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -28,9 +28,11 @@ #include #include +#include "AudioEngine.h" #include "GuiApplication.h" #include "AutomationEditor.h" #include "embed.h" +#include "gui_templates.h" #include "PathUtil.h" #include "SampleClip.h" #include "SampleLoader.h" @@ -77,9 +79,12 @@ void SampleClipView::constructContextMenu(QMenu* cm) { cm->addSeparator(); - /*contextMenu.addAction( embed::getIconPixmap( "record" ), - tr( "Set/clear record" ), - m_clip, SLOT(toggleRecord()));*/ + + QAction* recordToggleAction = cm->addAction(embed::getIconPixmap("record"), + tr("Set/clear record"), + m_clip, &SampleClip::toggleRecord); + + recordToggleAction->setEnabled(recordingCapabilitiesAvailable()); cm->addAction( embed::getIconPixmap("flip_x"), @@ -306,18 +311,34 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) } // recording sample tracks is not possible at the moment - /* if( m_clip->isRecord() ) + if (m_clip->isRecord()) { - p.setFont( pointSize<7>( p.font() ) ); + p.setFont(adjustedToPixelSize(p.font(), 10)); + + const auto fontHeight = p.fontMetrics().height(); + + const auto baseLine = height() - 3; + + constexpr int recordSymbolRadius = 3; + constexpr int recordSymbolCenterX = recordSymbolRadius + 4; + const int recordSymbolCenterY = baseLine - fontHeight / 2 + 1; + + constexpr int textStartX = recordSymbolCenterX + recordSymbolRadius + 4; + + auto textPos = QPoint(textStartX, baseLine); - p.setPen( textShadowColor() ); - p.drawText( 10, p.fontMetrics().height()+1, "Rec" ); - p.setPen( textColor() ); - p.drawText( 9, p.fontMetrics().height(), "Rec" ); + const auto rec = tr("Rec"); - p.setBrush( QBrush( textColor() ) ); - p.drawEllipse( 4, 5, 4, 4 ); - }*/ + p.setPen(textShadowColor()); + p.drawText(textPos + QPoint(1, 1), rec); + + p.setPen(textColor()); + p.drawText(textPos, rec); + + p.setBrush(QBrush(textColor())); + + p.drawEllipse(QPoint(recordSymbolCenterX, recordSymbolCenterY), recordSymbolRadius, recordSymbolRadius); + } p.end(); @@ -375,5 +396,9 @@ bool SampleClipView::splitClip( const TimePos pos ) else { return false; } } +bool SampleClipView::recordingCapabilitiesAvailable() const +{ + return Engine::audioEngine()->captureDeviceAvailable(); +} } // namespace lmms::gui diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index f6e9fc83bf8..15adce3e62b 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -35,7 +35,6 @@ #include #include "ActionGroup.h" -#include "AudioDevice.h" #include "AudioEngine.h" #include "AutomatableSlider.h" #include "ClipView.h" @@ -908,7 +907,7 @@ ComboBoxModel *SongEditor::snappingModel() const SongEditorWindow::SongEditorWindow(Song* song) : - Editor(Engine::audioEngine()->audioDev()->supportsCapture(), false), + Editor(true, false), m_editor(new SongEditor(song)), m_crtlAction( nullptr ), m_snapSizeLabel( new QLabel( m_toolBar ) ) @@ -1025,6 +1024,17 @@ SongEditorWindow::SongEditorWindow(Song* song) : connect(song, SIGNAL(projectLoaded()), this, SLOT(adjustUiAfterProjectLoad())); connect(this, SIGNAL(resized()), m_editor, SLOT(updatePositionLine())); + + // In case our current audio device does not support capture, + // disable the record buttons. + if (!Engine::audioEngine()->captureDeviceAvailable()) + { + for (auto &recordAction : {m_recordAccompanyAction, m_recordAction}) + { + recordAction->setEnabled(false); + recordAction->setToolTip(tr("Recording is unavailable: try connecting an input device or switching backend")); + } + } } QSize SongEditorWindow::sizeHint() const diff --git a/src/tracks/SampleTrack.cpp b/src/tracks/SampleTrack.cpp index 13050285668..be7393d2736 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 * @@ -22,7 +22,7 @@ * Boston, MA 02110-1301 USA. * */ - + #include "SampleTrack.h" #include @@ -106,7 +106,8 @@ bool SampleTrack::play( const TimePos & _start, const fpp_t _frames, if( _start >= sClip->startPosition() && _start < sClip->endPosition() ) { - if( sClip->isPlaying() == false && _start >= (sClip->startPosition() + sClip->startTimeOffset()) ) + if (!sClip->isPlaying() && (_start >= (sClip->startPosition() + sClip->startTimeOffset()) + || sClip->isRecord())) { auto bufferFramesPerTick = Engine::framesPerTick(sClip->sample().sampleRate()); f_cnt_t sampleStart = bufferFramesPerTick * ( _start - sClip->startPosition() - sClip->startTimeOffset() ); @@ -115,8 +116,16 @@ bool SampleTrack::play( const TimePos & _start, const fpp_t _frames, //if the Clip smaller than the sample length we play only until Clip end //else we play the sample to the end but nothing more f_cnt_t samplePlayLength = clipFrameLength > sampleBufferLength ? sampleBufferLength : clipFrameLength; + + // In case we are recoding, "play" the whole TCO. + if (sClip->isRecord()) + { + samplePlayLength = clipFrameLength; + } + //we only play within the sampleBuffer limits - if( sampleStart < sampleBufferLength ) + //Ignore that in case of recoding. + if (sampleStart < sampleBufferLength || sClip->isRecord()) { sClip->setSampleStartFrame( sampleStart ); sClip->setSamplePlayLength( samplePlayLength ); @@ -147,7 +156,7 @@ bool SampleTrack::play( const TimePos & _start, const fpp_t _frames, { return played_a_note; } - auto smpHandle = new SampleRecordHandle(st); + auto smpHandle = new SampleRecordHandle(st, _start - st->startPosition()); handle = smpHandle; } else