Skip to content

Commit 882fecb

Browse files
committed
Implement a virtual device rack
1 parent cdd195b commit 882fecb

31 files changed

Lines changed: 470 additions & 101 deletions

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ New features:
77

88
* Fix GitHub Issue #45: Implement a simple built-in sampler
99

10+
* Implements a virtual device rack
11+
1012
Bug fixes:
1113

1214
Other:

src/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ set(HEADER_FILES
3434
application/service/audio_worker.hpp
3535
application/service/automation_service.hpp
3636
application/service/copy_manager.hpp
37+
application/service/device_rack.hpp
38+
application/service/device_rack_controller.hpp
3739
application/service/device_service.hpp
3840
application/service/editor_service.hpp
3941
application/service/jack_service.hpp
@@ -151,6 +153,8 @@ set(SOURCE_FILES
151153
application/service/audio_worker.cpp
152154
application/service/automation_service.cpp
153155
application/service/copy_manager.cpp
156+
application/service/device_rack.cpp
157+
application/service/device_rack_controller.cpp
154158
application/service/device_service.cpp
155159
application/service/editor_service.cpp
156160
application/service/jack_service.cpp
@@ -260,6 +264,7 @@ set(QML_SOURCE_FILES
260264
${QML_BASE_DIR}/Dialogs/AddMidiCcAutomationDialog.qml
261265
${QML_BASE_DIR}/Dialogs/AddMidiCcSettingDialog.qml
262266
${QML_BASE_DIR}/Dialogs/AddPitchBendAutomationDialog.qml
267+
${QML_BASE_DIR}/Dialogs/DeviceRackDialog.qml
263268
${QML_BASE_DIR}/Dialogs/ColumnSettingsDialog.qml
264269
${QML_BASE_DIR}/Dialogs/ColumnSettingsDialog_InstrumentSettings.qml
265270
${QML_BASE_DIR}/Dialogs/ColumnSettingsDialog_MidiEffects.qml

src/application/application.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
#include "service/property_service.hpp"
4848
#include "service/recent_files_manager.hpp"
4949
#include "service/sampler_controller.hpp"
50+
#include "service/device_rack.hpp"
51+
#include "service/device_rack_controller.hpp"
5052
#include "service/selection_service.hpp"
5153
#include "service/settings_service.hpp"
5254
#include "service/side_chain_service.hpp"
@@ -90,7 +92,9 @@ Application::Application(int & argc, char ** argv)
9092
, m_editorService { std::make_shared<EditorService>(m_selectionService, m_settingsService, m_automationService) }
9193
, m_audioEngine { std::make_shared<AudioEngine>() }
9294
, m_deviceService { std::make_shared<DeviceService>(m_audioEngine) }
93-
, m_samplerController { std::make_shared<SamplerController>(std::make_shared<SamplerDevice>()) }
95+
, m_deviceRack { std::make_unique<DeviceRack>(m_deviceService) }
96+
, m_samplerController { std::make_shared<SamplerController>(nullptr) }
97+
, m_deviceRackController { std::make_shared<DeviceRackController>(m_deviceService, m_samplerController) }
9498
, m_jackService { std::make_shared<JackService>(m_settingsService, m_audioEngine) }
9599
, m_audioService { std::make_shared<AudioService>(m_settingsService, m_jackService, m_audioEngine) }
96100
, m_eventSelectionModel { std::make_shared<EventSelectionModel>() }
@@ -115,7 +119,10 @@ Application::Application(int & argc, char ** argv)
115119
, m_noteColumnModelHandler { std::make_unique<NoteColumnModelHandler>(m_editorService, m_selectionService, m_automationService, m_settingsService) }
116120
, m_engine { std::make_unique<QQmlApplicationEngine>() }
117121
{
118-
m_deviceService->registerDevice(m_samplerController->sampler());
122+
m_deviceRack->initialize();
123+
if (const auto sampler = std::dynamic_pointer_cast<SamplerDevice>(m_deviceService->device("Sampler 1"))) {
124+
m_samplerController->setSampler(sampler);
125+
}
119126
m_editorService->setMixerService(m_mixerService);
120127

121128
registerTypes();
@@ -147,6 +154,7 @@ void Application::registerTypes()
147154
qmlRegisterType<AudioSettingsModel>("Noteahead", majorVersion, minorVersion, "AudioSettingsModel");
148155
qmlRegisterType<AutomationService>("Noteahead", majorVersion, minorVersion, "AutomationService");
149156
qmlRegisterType<ColumnSettingsModel>("Noteahead", majorVersion, minorVersion, "ColumnSettingsModel");
157+
qmlRegisterType<DeviceRackController>("Noteahead", majorVersion, minorVersion, "DeviceRackController");
150158
qmlRegisterType<EditorService>("Noteahead", majorVersion, minorVersion, "EditorService");
151159
qmlRegisterType<EventSelectionModel>("Noteahead", majorVersion, minorVersion, "EventSelectionModel");
152160
qmlRegisterType<KeyboardService>("Noteahead", majorVersion, minorVersion, "KeyboardService");
@@ -181,6 +189,7 @@ void Application::setContextProperties()
181189
m_engine->rootContext()->setContextProperty("audioSettingsModel", m_audioSettingsModel.get());
182190
m_engine->rootContext()->setContextProperty("automationService", m_automationService.get());
183191
m_engine->rootContext()->setContextProperty("columnSettingsModel", m_columnSettingsModel.get());
192+
m_engine->rootContext()->setContextProperty("deviceRackController", m_deviceRackController.get());
184193
m_engine->rootContext()->setContextProperty("deviceService", m_deviceService.get());
185194
m_engine->rootContext()->setContextProperty("samplerController", m_samplerController.get());
186195
m_engine->rootContext()->setContextProperty("editorService", m_editorService.get());
@@ -320,6 +329,10 @@ void Application::connectServices()
320329

321330
void Application::connectDeviceService()
322331
{
332+
connect(m_deviceRackController.get(), &DeviceRackController::samplerDialogRequested, m_applicationService.get(), [this]() {
333+
emit m_applicationService->samplerDialogRequested();
334+
});
335+
323336
connect(m_editorService.get(), &EditorService::devicesSerializationRequested, m_deviceService.get(), &DeviceService::serializeToXml);
324337
connect(m_editorService.get(), &EditorService::devicesDeserializationRequested, m_deviceService.get(), &DeviceService::deserializeFromXml);
325338
connect(m_editorService.get(), &EditorService::projectPathChanged, m_deviceService.get(), &DeviceService::setProjectPath);

src/application/application.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class AudioService;
4141
class AudioSettingsModel;
4242
class AutomationService;
4343
class ColumnSettingsModel;
44+
class DeviceRack;
45+
class DeviceRackController;
4446
class DeviceService;
4547
class EditorService;
4648
class EventSelectionModel;
@@ -142,7 +144,9 @@ class Application : public QObject
142144

143145
std::shared_ptr<AudioEngine> m_audioEngine;
144146
std::shared_ptr<DeviceService> m_deviceService;
147+
std::unique_ptr<DeviceRack> m_deviceRack;
145148
std::shared_ptr<SamplerController> m_samplerController;
149+
std::shared_ptr<DeviceRackController> m_deviceRackController;
146150

147151
std::shared_ptr<JackService> m_jackService;
148152

src/application/models/sampler/sampler_pad_model.cpp

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace noteahead {
66

7-
SamplerPadModel::SamplerPadModel(std::shared_ptr<SamplerDevice> sampler, QObject * parent)
7+
SamplerPadModel::SamplerPadModel(SamplerDevice::SamplerDeviceS sampler, QObject * parent)
88
: QAbstractListModel { parent }
99
, m_sampler { std::move(sampler) }
1010
{
@@ -15,6 +15,25 @@ SamplerPadModel::SamplerPadModel(std::shared_ptr<SamplerDevice> sampler, QObject
1515
}
1616
}
1717

18+
void SamplerPadModel::setSampler(SamplerDevice::SamplerDeviceS sampler)
19+
{
20+
if (m_sampler == sampler) {
21+
return;
22+
}
23+
24+
beginResetModel();
25+
if (m_sampler) {
26+
m_sampler->disconnect(this);
27+
}
28+
m_sampler = std::move(sampler);
29+
if (m_sampler) {
30+
connect(m_sampler.get(), &SamplerDevice::dataChanged, this, [this]() {
31+
emit dataChanged(index(0), index(PadCount - 1), { NoteName, FilePath, IsLoaded });
32+
});
33+
}
34+
endResetModel();
35+
}
36+
1837
int SamplerPadModel::rowCount(const QModelIndex & parent) const
1938
{
2039
if (parent.isValid()) {

src/application/models/sampler/sampler_pad_model.hpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
#include <memory>
66
#include <vector>
77

8-
namespace noteahead {
8+
#include "../../../domain/devices/sampler_device.hpp"
99

10-
class SamplerDevice;
10+
namespace noteahead {
1111

1212
class SamplerPadModel : public QAbstractListModel
1313
{
@@ -22,7 +22,9 @@ class SamplerPadModel : public QAbstractListModel
2222
};
2323
Q_ENUM(Roles)
2424

25-
explicit SamplerPadModel(std::shared_ptr<SamplerDevice> sampler, QObject * parent = nullptr);
25+
explicit SamplerPadModel(SamplerDevice::SamplerDeviceS sampler, QObject * parent = nullptr);
26+
27+
void setSampler(SamplerDevice::SamplerDeviceS sampler);
2628

2729
int rowCount(const QModelIndex & parent = QModelIndex()) const override;
2830
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
@@ -31,7 +33,7 @@ class SamplerPadModel : public QAbstractListModel
3133
void updatePad(int padIndex);
3234

3335
private:
34-
std::shared_ptr<SamplerDevice> m_sampler;
36+
SamplerDevice::SamplerDeviceS m_sampler;
3537
static constexpr int PadCount = 16;
3638
static constexpr int StartNote = 36;
3739
};

src/application/service/application_service.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ void ApplicationService::requestRecentFilesDialog()
118118
emit recentFilesDialogRequested();
119119
}
120120

121+
void ApplicationService::requestDeviceRackDialog()
122+
{
123+
juzzlin::L(TAG).info() << "Device Rack requested";
124+
emit deviceRackDialogRequested();
125+
}
126+
121127
void ApplicationService::requestOpenProject()
122128
{
123129
juzzlin::L(TAG).info() << "'Open file' requested";

src/application/service/application_service.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class ApplicationService : public QObject
8989
Q_INVOKABLE void requestQuit();
9090

9191
Q_INVOKABLE void requestRecentFilesDialog();
92+
Q_INVOKABLE void requestDeviceRackDialog();
9293
Q_INVOKABLE void requestSaveProject();
9394
Q_INVOKABLE void requestSaveProjectAs();
9495
Q_INVOKABLE void requestSaveProjectAsTemplate();
@@ -148,6 +149,8 @@ class ApplicationService : public QObject
148149
void quitRequested();
149150

150151
void recentFilesDialogRequested();
152+
void deviceRackDialogRequested();
153+
void samplerDialogRequested();
151154

152155
void saveAsDialogRequested();
153156
void saveAsTemplateDialogRequested();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// This file is part of Noteahead.
2+
// Copyright (C) 2026 Jussi Lind <jussi.lind@iki.fi>
3+
//
4+
// Noteahead is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
// Noteahead is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with Noteahead. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#include "device_rack.hpp"
17+
#include "device_service.hpp"
18+
#include "../../domain/devices/sampler_device.hpp"
19+
20+
namespace noteahead {
21+
22+
DeviceRack::DeviceRack(DeviceServiceS deviceService)
23+
: m_deviceService { std::move(deviceService) }
24+
{
25+
}
26+
27+
DeviceRack::~DeviceRack() = default;
28+
29+
void DeviceRack::initialize()
30+
{
31+
for (size_t i = 1; i <= 4; i++) {
32+
const auto sampler = std::make_shared<SamplerDevice>("Sampler " + std::to_string(i));
33+
sampler->setId(i);
34+
m_deviceService->registerDevice(sampler);
35+
}
36+
}
37+
38+
} // namespace noteahead
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// This file is part of Noteahead.
2+
// Copyright (C) 2026 Jussi Lind <jussi.lind@iki.fi>
3+
//
4+
// Noteahead is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
// Noteahead is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with Noteahead. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#ifndef DEVICE_RACK_HPP
17+
#define DEVICE_RACK_HPP
18+
19+
#include <memory>
20+
#include <vector>
21+
22+
namespace noteahead {
23+
24+
class DeviceService;
25+
26+
class DeviceRack
27+
{
28+
public:
29+
using DeviceServiceS = std::shared_ptr<DeviceService>;
30+
explicit DeviceRack(DeviceServiceS deviceService);
31+
~DeviceRack();
32+
33+
void initialize();
34+
35+
private:
36+
DeviceServiceS m_deviceService;
37+
};
38+
39+
} // namespace noteahead
40+
41+
#endif // DEVICE_RACK_HPP

0 commit comments

Comments
 (0)