Skip to content

Commit c600092

Browse files
committed
Extend chord automation with an arpeggiator
1 parent 9ad49ca commit c600092

File tree

34 files changed

+638
-186
lines changed

34 files changed

+638
-186
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ New features:
88
* Implement the concept of a 'drum track'
99
- Drum tracks are not transposed by pattern => transpose
1010

11+
* Extend chord automation with an arpeggiator
12+
1113
Bug fixes:
1214

1315
Other:

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ All Arctic Music Project songs:
8888
- Linear, Sine wave, and Random modulation (since 1.8.0+)
8989
- Chord automation (since 1.2.0)
9090
- In Column Settings the user can set offsets for three notes that are relative to the root note
91+
- Arpeggiator (since 1.9.0)
92+
- Extend chord automation with an arpeggiator
93+
- Patterns: Up, Down, Up-Down, Down-Up, Random
94+
- Adjustable events per beat (1-32)
9195
- Step record / play notes via a MIDI controller (since 0.10.0)
9296
- The MIDI controller is routed to the instrument of the selected track
9397
- Highly adjustable note-off's (global default in ms, per-instrument in ms, manual)

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ set(HEADER_FILES
5454
common/utils.hpp
5555
domain/automation.hpp
5656
domain/automation_location.hpp
57+
domain/arpeggiator.hpp
5758
domain/column.hpp
5859
domain/column_settings.hpp
5960
domain/event.hpp
@@ -147,6 +148,7 @@ set(SOURCE_FILES
147148
common/utils.cpp
148149
domain/automation.cpp
149150
domain/automation_location.cpp
151+
domain/arpeggiator.cpp
150152
domain/column.cpp
151153
domain/column_settings.cpp
152154
domain/event.cpp

src/application/models/column_settings_model.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ void ColumnSettingsModel::reset()
4343
emit chordNote3OffsetChanged();
4444
emit chordNote3VelocityChanged();
4545
emit chordNote3DelayChanged();
46+
47+
emit arpeggiatorEnabledChanged();
48+
emit arpeggiatorPatternChanged();
49+
emit arpeggiatorEventsPerBeatChanged();
4650
}
4751

4852
void ColumnSettingsModel::requestData()
@@ -63,6 +67,10 @@ void ColumnSettingsModel::setColumnSettings(const ColumnSettings & settings)
6367
const bool note3VelocityChanged = m_settings.chordAutomationSettings.note3.velocity != settings.chordAutomationSettings.note3.velocity;
6468
const bool note3DelayChanged = m_settings.chordAutomationSettings.note3.delay != settings.chordAutomationSettings.note3.delay;
6569

70+
const bool arpeggiatorEnabledChanged = m_settings.chordAutomationSettings.arpeggiator.enabled != settings.chordAutomationSettings.arpeggiator.enabled;
71+
const bool arpeggiatorPatternChanged = m_settings.chordAutomationSettings.arpeggiator.pattern != settings.chordAutomationSettings.arpeggiator.pattern;
72+
const bool arpeggiatorEventsPerBeatChanged = m_settings.chordAutomationSettings.arpeggiator.eventsPerBeat != settings.chordAutomationSettings.arpeggiator.eventsPerBeat;
73+
6674
m_settings = settings;
6775

6876
if (delayChanged) {
@@ -96,6 +104,16 @@ void ColumnSettingsModel::setColumnSettings(const ColumnSettings & settings)
96104
emit chordNote3DelayChanged();
97105
}
98106

107+
if (arpeggiatorEnabledChanged) {
108+
emit this->arpeggiatorEnabledChanged();
109+
}
110+
if (arpeggiatorPatternChanged) {
111+
emit this->arpeggiatorPatternChanged();
112+
}
113+
if (arpeggiatorEventsPerBeatChanged) {
114+
emit this->arpeggiatorEventsPerBeatChanged();
115+
}
116+
99117
emit dataReceived();
100118
}
101119

@@ -255,4 +273,43 @@ void ColumnSettingsModel::setChordNote3Delay(qint16 delay)
255273
}
256274
}
257275

276+
bool ColumnSettingsModel::arpeggiatorEnabled() const
277+
{
278+
return m_settings.chordAutomationSettings.arpeggiator.enabled;
279+
}
280+
281+
void ColumnSettingsModel::setArpeggiatorEnabled(bool enabled)
282+
{
283+
if (m_settings.chordAutomationSettings.arpeggiator.enabled != enabled) {
284+
m_settings.chordAutomationSettings.arpeggiator.enabled = enabled;
285+
emit arpeggiatorEnabledChanged();
286+
}
287+
}
288+
289+
int ColumnSettingsModel::arpeggiatorPattern() const
290+
{
291+
return static_cast<int>(m_settings.chordAutomationSettings.arpeggiator.pattern);
292+
}
293+
294+
void ColumnSettingsModel::setArpeggiatorPattern(int pattern)
295+
{
296+
if (static_cast<int>(m_settings.chordAutomationSettings.arpeggiator.pattern) != pattern) {
297+
m_settings.chordAutomationSettings.arpeggiator.pattern = static_cast<Arpeggiator::Pattern>(pattern);
298+
emit arpeggiatorPatternChanged();
299+
}
300+
}
301+
302+
uint8_t ColumnSettingsModel::arpeggiatorEventsPerBeat() const
303+
{
304+
return m_settings.chordAutomationSettings.arpeggiator.eventsPerBeat;
305+
}
306+
307+
void ColumnSettingsModel::setArpeggiatorEventsPerBeat(uint8_t eventsPerBeat)
308+
{
309+
if (m_settings.chordAutomationSettings.arpeggiator.eventsPerBeat != eventsPerBeat) {
310+
m_settings.chordAutomationSettings.arpeggiator.eventsPerBeat = eventsPerBeat;
311+
emit arpeggiatorEventsPerBeatChanged();
312+
}
313+
}
314+
258315
} // namespace noteahead

src/application/models/column_settings_model.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class ColumnSettingsModel : public QObject
4141
Q_PROPERTY(quint8 chordNote3Velocity READ chordNote3Velocity WRITE setChordNote3Velocity NOTIFY chordNote3VelocityChanged)
4242
Q_PROPERTY(qint16 chordNote3Delay READ chordNote3Delay WRITE setChordNote3Delay NOTIFY chordNote3DelayChanged)
4343

44+
Q_PROPERTY(bool arpeggiatorEnabled READ arpeggiatorEnabled WRITE setArpeggiatorEnabled NOTIFY arpeggiatorEnabledChanged)
45+
Q_PROPERTY(int arpeggiatorPattern READ arpeggiatorPattern WRITE setArpeggiatorPattern NOTIFY arpeggiatorPatternChanged)
46+
Q_PROPERTY(uint8_t arpeggiatorEventsPerBeat READ arpeggiatorEventsPerBeat WRITE setArpeggiatorEventsPerBeat NOTIFY arpeggiatorEventsPerBeatChanged)
47+
4448
public:
4549
explicit ColumnSettingsModel(QObject * parent = nullptr);
4650
~ColumnSettingsModel() override;
@@ -87,6 +91,15 @@ class ColumnSettingsModel : public QObject
8791
qint16 chordNote3Delay() const;
8892
void setChordNote3Delay(qint16 delay);
8993

94+
bool arpeggiatorEnabled() const;
95+
void setArpeggiatorEnabled(bool enabled);
96+
97+
int arpeggiatorPattern() const;
98+
void setArpeggiatorPattern(int pattern);
99+
100+
uint8_t arpeggiatorEventsPerBeat() const;
101+
void setArpeggiatorEventsPerBeat(uint8_t eventsPerBeat);
102+
90103
signals:
91104
void dataRequested();
92105
void dataReceived();
@@ -106,6 +119,10 @@ class ColumnSettingsModel : public QObject
106119
void chordNote3VelocityChanged();
107120
void chordNote3DelayChanged();
108121

122+
void arpeggiatorEnabledChanged();
123+
void arpeggiatorPatternChanged();
124+
void arpeggiatorEventsPerBeatChanged();
125+
109126
void saveRequested(quint64 trackIndex, quint64 columnIndex, const ColumnSettings & settings);
110127

111128
private:

src/common/constants.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,21 @@ QString xmlKeyChordNote3Delay()
189189
return "chordNote3Delay";
190190
}
191191

192+
QString xmlKeyArpeggiatorEnabled()
193+
{
194+
return "arpeggiatorEnabled";
195+
}
196+
197+
QString xmlKeyArpeggiatorPattern()
198+
{
199+
return "arpeggiatorPattern";
200+
}
201+
202+
QString xmlKeyArpeggiatorEventsPerBeat()
203+
{
204+
return "arpeggiatorEventsPerBeat";
205+
}
206+
192207
QString xmlKeyController()
193208
{
194209
return "controller";

src/common/constants.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ QString xmlKeyChordNote2Delay();
6868
QString xmlKeyChordNote3Offset();
6969
QString xmlKeyChordNote3Velocity();
7070
QString xmlKeyChordNote3Delay();
71+
QString xmlKeyArpeggiatorEnabled();
72+
QString xmlKeyArpeggiatorPattern();
73+
QString xmlKeyArpeggiatorEventsPerBeat();
7174

7275
QString xmlKeyController();
7376
QString xmlKeyEnabled();

src/domain/arpeggiator.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 "arpeggiator.hpp"
17+
18+
#include "../application/service/random_service.hpp"
19+
20+
#include <algorithm>
21+
22+
namespace noteahead {
23+
24+
Arpeggiator::NoteInfoList Arpeggiator::generate(Pattern pattern, const NoteInfoList & inputNotes)
25+
{
26+
if (inputNotes.empty()) {
27+
return {};
28+
}
29+
30+
NoteInfoList uniqueNotes = inputNotes;
31+
std::sort(uniqueNotes.begin(), uniqueNotes.end(), [](auto && a, auto && b) {
32+
return a.note < b.note;
33+
});
34+
uniqueNotes.erase(std::unique(uniqueNotes.begin(), uniqueNotes.end(), [](auto && a, auto && b) {
35+
return a.note == b.note;
36+
}), uniqueNotes.end());
37+
38+
NoteInfoList arpeggioSequence;
39+
switch (pattern) {
40+
case Pattern::Up:
41+
arpeggioSequence = uniqueNotes;
42+
break;
43+
case Pattern::Down:
44+
arpeggioSequence = uniqueNotes;
45+
std::reverse(arpeggioSequence.begin(), arpeggioSequence.end());
46+
break;
47+
case Pattern::UpDown:
48+
arpeggioSequence = uniqueNotes;
49+
for (int i = static_cast<int>(uniqueNotes.size()) - 2; i > 0; i--) {
50+
arpeggioSequence.push_back(uniqueNotes[static_cast<size_t>(i)]);
51+
}
52+
break;
53+
case Pattern::DownUp:
54+
arpeggioSequence = uniqueNotes;
55+
std::reverse(arpeggioSequence.begin(), arpeggioSequence.end());
56+
for (size_t i = 1; i < uniqueNotes.size() - 1; i++) {
57+
arpeggioSequence.push_back(uniqueNotes[i]);
58+
}
59+
break;
60+
case Pattern::Random:
61+
arpeggioSequence = uniqueNotes;
62+
std::shuffle(arpeggioSequence.begin(), arpeggioSequence.end(), RandomService::generator());
63+
break;
64+
}
65+
66+
return arpeggioSequence;
67+
}
68+
69+
} // namespace noteahead

src/domain/arpeggiator.hpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 ARPEGGIATOR_HPP
17+
#define ARPEGGIATOR_HPP
18+
19+
#include <cstdint>
20+
#include <vector>
21+
22+
namespace noteahead {
23+
24+
class Arpeggiator
25+
{
26+
public:
27+
enum class Pattern
28+
{
29+
Up,
30+
Down,
31+
UpDown,
32+
DownUp,
33+
Random
34+
};
35+
36+
struct Settings
37+
{
38+
bool enabled = false;
39+
Pattern pattern = Pattern::Up;
40+
uint8_t eventsPerBeat = 4;
41+
};
42+
43+
struct NoteInfo
44+
{
45+
uint8_t note = 0;
46+
uint8_t velocity = 0;
47+
};
48+
49+
using NoteInfoList = std::vector<NoteInfo>;
50+
static NoteInfoList generate(Pattern pattern, const NoteInfoList & inputNotes);
51+
};
52+
53+
} // namespace noteahead
54+
55+
#endif // ARPEGGIATOR_HPP

src/domain/column_settings.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ void ColumnSettings::serializeToXml(QXmlStreamWriter & writer) const
4646
writer.writeAttribute(Constants::NahdXml::xmlKeyChordNote3Velocity(), QString::number(chordAutomationSettings.note3.velocity));
4747
writer.writeAttribute(Constants::NahdXml::xmlKeyChordNote3Delay(), QString::number(chordAutomationSettings.note3.delay));
4848

49-
writer.writeEndElement();
49+
writer.writeAttribute(Constants::NahdXml::xmlKeyArpeggiatorEnabled(), chordAutomationSettings.arpeggiator.enabled ? Constants::NahdXml::xmlValueTrue() : Constants::NahdXml::xmlValueFalse());
50+
writer.writeAttribute(Constants::NahdXml::xmlKeyArpeggiatorPattern(), QString::number(static_cast<int>(chordAutomationSettings.arpeggiator.pattern)));
51+
writer.writeAttribute(Constants::NahdXml::xmlKeyArpeggiatorEventsPerBeat(), QString::number(chordAutomationSettings.arpeggiator.eventsPerBeat));
52+
53+
writer.writeEndElement(); // ColumnSettings
5054
}
5155

5256
ColumnSettings::ColumnSettingsU ColumnSettings::deserializeFromXml(QXmlStreamReader & reader)
@@ -67,6 +71,10 @@ ColumnSettings::ColumnSettingsU ColumnSettings::deserializeFromXml(QXmlStreamRea
6771
settings->chordAutomationSettings.note3.velocity = Utils::Xml::readUIntAttribute(reader, Constants::NahdXml::xmlKeyChordNote3Velocity(), false).value_or(100);
6872
settings->chordAutomationSettings.note3.delay = Utils::Xml::readIntAttribute(reader, Constants::NahdXml::xmlKeyChordNote3Delay(), false).value_or(0);
6973

74+
settings->chordAutomationSettings.arpeggiator.enabled = Utils::Xml::readBoolAttribute(reader, Constants::NahdXml::xmlKeyArpeggiatorEnabled(), false).value_or(false);
75+
settings->chordAutomationSettings.arpeggiator.pattern = static_cast<Arpeggiator::Pattern>(Utils::Xml::readIntAttribute(reader, Constants::NahdXml::xmlKeyArpeggiatorPattern(), false).value_or(0));
76+
settings->chordAutomationSettings.arpeggiator.eventsPerBeat = static_cast<uint8_t>(Utils::Xml::readUIntAttribute(reader, Constants::NahdXml::xmlKeyArpeggiatorEventsPerBeat(), false).value_or(4));
77+
7078
return settings;
7179
}
7280

0 commit comments

Comments
 (0)