Skip to content

Commit 2052ad3

Browse files
committed
Implement audio playback for the audio recorder
- Output device can be selected in the audio settings - Clicking on the waveform will make the editor jump to the corresponding location - The last recorded file path is saved and loaded along the project
1 parent 1ff4ceb commit 2052ad3

34 files changed

Lines changed: 860 additions & 92 deletions

Agents.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ Adherence to these standards is mandatory for all contributions:
5959
- **Execution**: Run tests using `ctest` from the build directory.
6060
- **Framework**: Uses the Qt Test framework.
6161

62+
## 🚀 Build & Test
63+
64+
- **Build**: Use `cmake --build build` to build the project.
65+
- **Run Tests**: Use `ctest --test-dir build` to run all tests.
66+
6267
## 🚀 Performance & Timing
6368

6469
- **Accuracy**: Noteahead renders events just before playback to ensure jitter-free, drift-free timing.

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ New features:
88
* Add song transposition feature
99
- Fixes also drum track not taken into account properly
1010

11+
* Implement audio playback for the audio recorder
12+
- Output device can be selected in the audio settings
13+
- Clicking on the waveform will make the editor jump to the corresponding location
14+
- The last recorded file path is saved and loaded along the project
15+
1116
Bug fixes:
1217

1318
Other:

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ All Arctic Music Project songs:
107107
- Audio recorder
108108
- Just enable recording in `Settings => Audio` and Noteahead will record from the selected audio source when the song starts and name the file according to active tracks
109109
- Records audio from Jack if Jack Transport is enabled (since 1.8.0)
110+
- Clicking on the waveform will make the editor jump to the corresponding location (since 2.1.0)
111+
- The last recorded audio file path is saved and loaded along the project (since 2.1.0)
112+
- Audio player (since 2.1.0)
113+
- Output device can be selected in the audio settings
110114

111115
### Tools
112116
- Delay time calculator

src/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,11 @@ set(SOURCE_FILES
178178
domain/song.cpp
179179
domain/track.cpp
180180
infra/audio/audio_recorder.cpp
181+
infra/audio/audio_player.cpp
181182
infra/audio/implementation/jack/audio_recorder_jack.cpp
183+
infra/audio/implementation/jack/audio_player_jack.cpp
182184
infra/audio/implementation/librtaudio/audio_recorder_rt_audio.cpp
185+
infra/audio/implementation/librtaudio/audio_player_rt_audio.cpp
183186
infra/midi/export/midi_exporter.cpp
184187
infra/midi/import/midi_importer.cpp
185188
infra/midi/implementation/librtmidi/midi_in_rt_midi.cpp

src/application/application.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@ void Application::connectEditorService()
427427
connect(m_editorService.get(), &EditorService::mixerSerializationRequested, m_mixerService.get(), &MixerService::serializeToXml);
428428
connect(m_editorService.get(), &EditorService::sideChainDeserializationRequested, m_sideChainService.get(), &SideChainService::deserializeFromXml);
429429
connect(m_editorService.get(), &EditorService::sideChainSerializationRequested, m_sideChainService.get(), &SideChainService::serializeToXml);
430+
connect(m_editorService.get(), &EditorService::audioRecorderDeserializationRequested, m_audioService.get(), &AudioService::deserializeFromXml);
431+
connect(m_editorService.get(), &EditorService::audioRecorderSerializationRequested, m_audioService.get(), &AudioService::serializeToXml);
430432
connect(m_editorService.get(), &EditorService::patternsDeleted, m_automationService.get(), &AutomationService::deletePatterns);
431433
connect(m_editorService.get(), &EditorService::patternsDeleted, m_noteColumnModelHandler.get(), &NoteColumnModelHandler::deletePatterns);
432434
connect(m_editorService.get(), &EditorService::trackDeleted, m_sideChainService.get(), &SideChainService::removeSettings);
@@ -551,7 +553,7 @@ void Application::connectPlayerService()
551553
const auto isPlaying = m_playerService->isPlaying();
552554
m_midiService->setIsPlaying(isPlaying);
553555
if (m_settingsService->recordingEnabled()) {
554-
applyAudioRecording(isPlaying);
556+
applyAudioRecording(isPlaying, m_playerService->tick());
555557
}
556558
});
557559

@@ -597,17 +599,17 @@ QString Application::buildAudioFileName() const
597599
return {};
598600
}
599601

600-
void Application::applyAudioRecording(bool isPlaying)
602+
void Application::applyAudioRecording(bool isPlaying, quint64 startTick)
601603
{
602604
if (isPlaying) {
603605
if (const auto audioFileName = buildAudioFileName(); !audioFileName.isEmpty()) {
604606
juzzlin::L(TAG).info() << "Recording audio to " << std::quoted(audioFileName.toStdString());
605-
m_audioService->startRecording(audioFileName, static_cast<uint32_t>(m_settingsService->audioBufferSize()));
607+
m_audioService->startRecording(audioFileName, static_cast<uint32_t>(m_settingsService->audioBufferSize()), startTick);
606608
} else {
607609
juzzlin::L(TAG).error() << "Output audio filename is empty!";
608610
}
609611
} else {
610-
m_audioService->stopRecording();
612+
m_audioService->stopRecording(startTick);
611613
}
612614
}
613615

src/application/application.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class Application : public QObject
7878
private:
7979
void applyAllInstruments();
8080
void applyAllMidiCcSettings();
81-
void applyAudioRecording(bool isPlaying);
81+
void applyAudioRecording(bool isPlaying, quint64 startTick);
8282
QString buildAudioFileName() const;
8383
void applyInstrument(size_t trackIndex, const Instrument & instrument);
8484
void applyMidiCcSettings(const Instrument & instrument);

src/application/models/audio_settings_model.cpp

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@ AudioSettingsModel::AudioSettingsModel(AudioServiceS audioService, SettingsServi
2929
, m_settingsService { std::move(settingsService) }
3030
{
3131
m_selectedInputDeviceId = m_settingsService->audioInputDeviceId();
32-
// Ensure the audio service knows about the saved device ID
3332
m_audioService->setInputDevice(m_selectedInputDeviceId);
3433
refreshInputDevices();
3534

36-
connect(m_audioService.get(), &AudioService::reinitialized, this, &AudioSettingsModel::refreshInputDevices);
35+
m_selectedOutputDeviceId = m_settingsService->audioOutputDeviceId();
36+
m_audioService->setOutputDevice(m_selectedOutputDeviceId);
37+
refreshOutputDevices();
38+
39+
connect(m_audioService.get(), &AudioService::reinitialized, this, [this]() {
40+
refreshInputDevices();
41+
refreshOutputDevices();
42+
});
3743
}
3844

3945
AudioSettingsModel::~AudioSettingsModel() = default;
@@ -59,11 +65,39 @@ void AudioSettingsModel::setSelectedInputDeviceId(int deviceId)
5965
}
6066
}
6167

68+
QVariantList AudioSettingsModel::outputDevices() const
69+
{
70+
return m_outputDevices;
71+
}
72+
73+
int AudioSettingsModel::selectedOutputDeviceId() const
74+
{
75+
return m_selectedOutputDeviceId;
76+
}
77+
78+
void AudioSettingsModel::setSelectedOutputDeviceId(int deviceId)
79+
{
80+
if (m_selectedOutputDeviceId != deviceId) {
81+
juzzlin::L(TAG).info() << "Selecting audio output device ID: " << deviceId;
82+
m_selectedOutputDeviceId = deviceId;
83+
m_settingsService->setAudioOutputDeviceId(deviceId);
84+
m_audioService->setOutputDevice(deviceId);
85+
emit selectedOutputDeviceIdChanged(deviceId);
86+
}
87+
}
88+
6289
void AudioSettingsModel::refreshInputDevices()
6390
{
6491
juzzlin::L(TAG).info() << "Refreshing input devices...";
6592
m_inputDevices = m_audioService->getInputDevices();
6693
emit inputDevicesChanged();
6794
}
6895

96+
void AudioSettingsModel::refreshOutputDevices()
97+
{
98+
juzzlin::L(TAG).info() << "Refreshing output devices...";
99+
m_outputDevices = m_audioService->getOutputDevices();
100+
emit outputDevicesChanged();
101+
}
102+
69103
} // namespace noteahead

src/application/models/audio_settings_model.hpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class AudioSettingsModel : public QObject
3232

3333
Q_PROPERTY(QVariantList inputDevices READ inputDevices NOTIFY inputDevicesChanged)
3434
Q_PROPERTY(int selectedInputDeviceId READ selectedInputDeviceId WRITE setSelectedInputDeviceId NOTIFY selectedInputDeviceIdChanged)
35+
Q_PROPERTY(QVariantList outputDevices READ outputDevices NOTIFY outputDevicesChanged)
36+
Q_PROPERTY(int selectedOutputDeviceId READ selectedOutputDeviceId WRITE setSelectedOutputDeviceId NOTIFY selectedOutputDeviceIdChanged)
3537

3638
public:
3739
using AudioServiceS = std::shared_ptr<AudioService>;
@@ -41,22 +43,30 @@ class AudioSettingsModel : public QObject
4143
~AudioSettingsModel() override;
4244

4345
QVariantList inputDevices() const;
44-
4546
int selectedInputDeviceId() const;
4647
void setSelectedInputDeviceId(int deviceId);
47-
48+
49+
QVariantList outputDevices() const;
50+
int selectedOutputDeviceId() const;
51+
void setSelectedOutputDeviceId(int deviceId);
52+
4853
Q_INVOKABLE void refreshInputDevices();
54+
Q_INVOKABLE void refreshOutputDevices();
4955

5056
signals:
5157
void inputDevicesChanged();
5258
void selectedInputDeviceIdChanged(int deviceId);
59+
void outputDevicesChanged();
60+
void selectedOutputDeviceIdChanged(int deviceId);
5361

5462
private:
5563
AudioServiceS m_audioService;
5664
SettingsServiceS m_settingsService;
5765

5866
QVariantList m_inputDevices;
5967
int m_selectedInputDeviceId = 0;
68+
QVariantList m_outputDevices;
69+
int m_selectedOutputDeviceId = 0;
6070
};
6171

6272
} // namespace noteahead

0 commit comments

Comments
 (0)