diff --git a/include/AudioEngine.h b/include/AudioEngine.h index f6d8692b52d..0163ba84bb2 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -170,6 +170,8 @@ class LMMS_EXPORT AudioEngine : public QObject return m_audioDev; } + bool captureDeviceAvailable() const; + // audio-port-stuff inline void addAudioPort(AudioPort * port) @@ -341,6 +343,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/AudioJack.h b/include/AudioJack.h index 234f6ebf234..b84c337ca78 100644 --- a/include/AudioJack.h +++ b/include/AudioJack.h @@ -100,6 +100,7 @@ private slots: int processCallback(jack_nframes_t nframes); + static int setBufferSizeCallback(jack_nframes_t nframes, void* udata); static int staticProcessCallback(jack_nframes_t nframes, void* udata); static void shutdownCallback(void* _udata); @@ -110,8 +111,11 @@ private slots: std::atomic m_midiClient; std::vector m_outputPorts; + std::vector m_inputPorts; jack_default_audio_sample_t** m_tempOutBufs; + jack_default_audio_sample_t* m_inputFrameBuffer; SampleFrame* m_outBuf; + SampleFrame* m_inBuf; f_cnt_t m_framesDoneInCurBuf; f_cnt_t m_framesToDoInCurBuf; 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 0da65f426a4..94c6c75fd30 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -326,7 +326,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 ); @@ -389,7 +389,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 ); @@ -581,6 +581,10 @@ void AudioEngine::restoreAudioDevice() } +bool AudioEngine::captureDeviceAvailable() const +{ + return audioDev()->supportsCapture(); +} void AudioEngine::removeAudioPort(AudioPort * port) @@ -604,7 +608,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; } @@ -625,7 +629,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 @@ -684,7 +688,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 8bd3a776bfc..b0015f2590d 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/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index bd5b8e514de..4b2840cfb81 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -57,6 +57,7 @@ AudioJack::AudioJack(bool& successful, AudioEngine* audioEngineParam) , m_midiClient(nullptr) , m_tempOutBufs(new jack_default_audio_sample_t*[channels()]) , m_outBuf(new SampleFrame[audioEngine()->framesPerPeriod()]) + , m_inBuf(new SampleFrame[audioEngine()->framesPerPeriod()]) , m_framesDoneInCurBuf(0) , m_framesToDoInCurBuf(0) { @@ -66,6 +67,8 @@ AudioJack::AudioJack(bool& successful, AudioEngine* audioEngineParam) if (successful) { connect(this, SIGNAL(zombified()), this, SLOT(restartAfterZombified()), Qt::QueuedConnection); } + + m_supportsCapture = true; } @@ -90,6 +93,7 @@ AudioJack::~AudioJack() delete[] m_tempOutBufs; delete[] m_outBuf; + delete[] m_inBuf; } @@ -154,6 +158,10 @@ bool AudioJack::initJackClient() clientName.toLatin1().constData(), jack_get_client_name(m_client)); } + m_inputFrameBuffer = new jack_default_audio_sample_t[channels() * jack_get_buffer_size(m_client)]; + + jack_set_buffer_size_callback(m_client, setBufferSizeCallback, this); + // set process-callback jack_set_process_callback(m_client, staticProcessCallback, this); @@ -167,6 +175,10 @@ bool AudioJack::initJackClient() QString name = QString("master out ") + ((ch % 2) ? "R" : "L") + QString::number(ch / 2 + 1); m_outputPorts.push_back( jack_port_register(m_client, name.toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0)); + + QString input_name = QString("master in ") + ((ch % 2) ? "R" : "L") + QString::number(ch / 2 + 1); + m_inputPorts.push_back(jack_port_register(m_client, input_name.toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0)); + if (m_outputPorts.back() == nullptr) { printf("no more JACK-ports available!\n"); @@ -286,11 +298,17 @@ void AudioJack::renamePort(AudioPort* port) } +int AudioJack::setBufferSizeCallback(jack_nframes_t nframes, void* udata) +{ + auto thisClass = static_cast(udata); + delete[] thisClass->m_inputFrameBuffer; + thisClass->m_inputFrameBuffer = new jack_default_audio_sample_t[thisClass->channels() * nframes]; + return 0; +} int AudioJack::processCallback(jack_nframes_t nframes) { - // do midi processing first so that midi input can // add to the following sound processing if (m_midiClient && nframes > 0) @@ -356,6 +374,16 @@ int AudioJack::processCallback(jack_nframes_t nframes) } } + for (int c = 0; c < channels(); ++c) + { + jack_default_audio_sample_t* jack_input_buffer = (jack_default_audio_sample_t*) jack_port_get_buffer(m_inputPorts[c], nframes); + + for (jack_nframes_t frame = 0; frame < nframes; frame++) + { + m_inputFrameBuffer[frame * channels() + c] = jack_input_buffer[frame]; + } + } + audioEngine()->pushInputFrames ((SampleFrame*) m_inputFrameBuffer, nframes); return 0; } diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index a7251be8de6..9e519869e09 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 "FontHelper.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(); @@ -376,5 +397,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 72ee28bc82a..fc6fc14633f 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" @@ -912,7 +911,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 ) ) @@ -1029,6 +1028,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 8ad75799dd0..ddf5146144e 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