Skip to content

Commit 5986943

Browse files
committed
Implement random modulation for pitch bend automations
1 parent 15af83b commit 5986943

22 files changed

Lines changed: 739 additions & 217 deletions

CHANGELOG

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

1010
* Implement Random modulation for MIDI CC automations
1111

12+
* Implement Random modulation for pitch bend automations
13+
1214
Bug fixes:
1315

1416
Other:

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ set(QML_SOURCE_FILES
229229
${QML_BASE_DIR}/Dialogs/MidiCcSelector.qml
230230
${QML_BASE_DIR}/Dialogs/MidiExportDialog.qml
231231
${QML_BASE_DIR}/Dialogs/MidiImportDialog.qml
232+
${QML_BASE_DIR}/Dialogs/ModulationGroupBox.qml
232233
${QML_BASE_DIR}/Dialogs/NoteFrequencyDialog.qml
233234
${QML_BASE_DIR}/Dialogs/PitchBendAutomationModel.qml
234235
${QML_BASE_DIR}/Dialogs/RecentFilesDialog.qml

src/application/models/midi_cc_automations_model.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ bool MidiCcAutomationsModel::setData(const QModelIndex & index, const QVariant &
248248
} break;
249249
case DataRole::Modulation_Type: {
250250
auto modulation = midiCcAutomation.modulation();
251-
if (const auto newType = static_cast<MidiCcAutomation::ModulationParameters::ModulationType>(value.toInt()); modulation.type != newType) {
251+
if (const auto newType = static_cast<ModulationParameters::ModulationType>(value.toInt()); modulation.type != newType) {
252252
modulation.type = newType;
253253
midiCcAutomation.setModulation(modulation);
254254
changed = true;
@@ -376,7 +376,7 @@ void MidiCcAutomationsModel::changeModulationType(int index, int type)
376376
auto && midiCcAutomation = m_midiCcAutomations[static_cast<size_t>(index)];
377377
auto modulation = midiCcAutomation.modulation();
378378
if (static_cast<int>(modulation.type) != type) {
379-
modulation.type = static_cast<MidiCcAutomation::ModulationParameters::ModulationType>(type);
379+
modulation.type = static_cast<ModulationParameters::ModulationType>(type);
380380
midiCcAutomation.setModulation(modulation);
381381
m_midiCcAutomationsChanged.erase(midiCcAutomation);
382382
m_midiCcAutomationsChanged.insert(midiCcAutomation);

src/application/models/pitch_bend_automations_model.cpp

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ PitchBendAutomationsModel::PitchBendAutomationList PitchBendAutomationsModel::fi
9696

9797
void PitchBendAutomationsModel::setPitchBendAutomations(PitchBendAutomationList PitchBendAutomations)
9898
{
99-
juzzlin::L(TAG).info() << "Setting MIDI CC automations: " << PitchBendAutomations.size() << " found";
99+
juzzlin::L(TAG).info() << "Setting pitch bend automations: " << PitchBendAutomations.size() << " found";
100100
beginResetModel();
101101
m_pitchBendAutomationsChanged.clear();
102102
m_pitchBendAutomationsDeleted.clear();
@@ -136,6 +136,16 @@ QVariant PitchBendAutomationsModel::data(const QModelIndex & index, int role) co
136136
return static_cast<quint64>(PitchBendAutomation.location().track());
137137
case DataRole::Column:
138138
return static_cast<quint64>(PitchBendAutomation.location().column());
139+
case DataRole::Modulation_Sine_Cycles:
140+
return PitchBendAutomation.modulation().cycles;
141+
case DataRole::Modulation_Sine_Amplitude:
142+
return PitchBendAutomation.modulation().amplitude;
143+
case DataRole::Modulation_Sine_Offset:
144+
return PitchBendAutomation.modulation().offset;
145+
case DataRole::Modulation_Sine_Inverted:
146+
return PitchBendAutomation.modulation().inverted;
147+
case DataRole::Modulation_Type:
148+
return static_cast<int>(PitchBendAutomation.modulation().type);
139149
}
140150
}
141151
return "N/A";
@@ -194,6 +204,46 @@ bool PitchBendAutomationsModel::setData(const QModelIndex & index, const QVarian
194204
changed = true;
195205
}
196206
} break;
207+
case DataRole::Modulation_Sine_Cycles: {
208+
auto modulation = PitchBendAutomation.modulation();
209+
if (const auto newCycles = value.toUInt(); modulation.cycles != newCycles) {
210+
modulation.cycles = newCycles;
211+
PitchBendAutomation.setModulation(modulation);
212+
changed = true;
213+
}
214+
} break;
215+
case DataRole::Modulation_Sine_Amplitude: {
216+
auto modulation = PitchBendAutomation.modulation();
217+
if (const auto newAmplitude = value.toFloat(); modulation.amplitude != newAmplitude) {
218+
modulation.amplitude = newAmplitude;
219+
PitchBendAutomation.setModulation(modulation);
220+
changed = true;
221+
}
222+
} break;
223+
case DataRole::Modulation_Sine_Offset: {
224+
auto modulation = PitchBendAutomation.modulation();
225+
if (const auto newOffset = value.toFloat(); modulation.offset != newOffset) {
226+
modulation.offset = newOffset;
227+
PitchBendAutomation.setModulation(modulation);
228+
changed = true;
229+
}
230+
} break;
231+
case DataRole::Modulation_Sine_Inverted: {
232+
auto modulation = PitchBendAutomation.modulation();
233+
if (const auto newInverted = value.toBool(); modulation.inverted != newInverted) {
234+
modulation.inverted = newInverted;
235+
PitchBendAutomation.setModulation(modulation);
236+
changed = true;
237+
}
238+
} break;
239+
case DataRole::Modulation_Type: {
240+
auto modulation = PitchBendAutomation.modulation();
241+
if (const auto newType = static_cast<ModulationParameters::ModulationType>(value.toInt()); modulation.type != newType) {
242+
modulation.type = newType;
243+
PitchBendAutomation.setModulation(modulation);
244+
changed = true;
245+
}
246+
} break;
197247
case DataRole::Pattern:
198248
case DataRole::Track:
199249
case DataRole::Column:
@@ -202,6 +252,7 @@ bool PitchBendAutomationsModel::setData(const QModelIndex & index, const QVarian
202252
}
203253

204254
if (changed) {
255+
m_pitchBendAutomations[static_cast<size_t>(index.row())] = PitchBendAutomation;
205256
m_pitchBendAutomationsChanged.erase(PitchBendAutomation);
206257
m_pitchBendAutomationsChanged.insert(PitchBendAutomation);
207258
emit dataChanged(index, index, { role });
@@ -240,7 +291,12 @@ QHash<int, QByteArray> PitchBendAutomationsModel::roleNames() const
240291
{ static_cast<int>(DataRole::Pattern), "pattern" },
241292
{ static_cast<int>(DataRole::Track), "track" },
242293
{ static_cast<int>(DataRole::Value0), "value0" },
243-
{ static_cast<int>(DataRole::Value1), "value1" }
294+
{ static_cast<int>(DataRole::Value1), "value1" },
295+
{ static_cast<int>(DataRole::Modulation_Sine_Cycles), "modulationSineCycles" },
296+
{ static_cast<int>(DataRole::Modulation_Sine_Amplitude), "modulationSineAmplitude" },
297+
{ static_cast<int>(DataRole::Modulation_Sine_Offset), "modulationSineOffset" },
298+
{ static_cast<int>(DataRole::Modulation_Sine_Inverted), "modulationSineInverted" },
299+
{ static_cast<int>(DataRole::Modulation_Type), "modulationType" }
244300
};
245301
}
246302

@@ -256,4 +312,21 @@ void PitchBendAutomationsModel::applyAll()
256312
m_pitchBendAutomationsDeleted.clear();
257313
}
258314

315+
void PitchBendAutomationsModel::changeModulationType(int index, int type)
316+
{
317+
if (index >= 0 && static_cast<size_t>(index) < m_pitchBendAutomations.size()) {
318+
auto && pitchBendAutomation = m_pitchBendAutomations[static_cast<size_t>(index)];
319+
auto modulation = pitchBendAutomation.modulation();
320+
if (static_cast<int>(modulation.type) != type) {
321+
modulation.type = static_cast<ModulationParameters::ModulationType>(type);
322+
pitchBendAutomation.setModulation(modulation);
323+
m_pitchBendAutomations[static_cast<size_t>(index)] = pitchBendAutomation;
324+
m_pitchBendAutomationsChanged.erase(pitchBendAutomation);
325+
m_pitchBendAutomationsChanged.insert(pitchBendAutomation);
326+
emit dataChanged(this->index(index), this->index(index), { static_cast<int>(DataRole::Modulation_Type) });
327+
juzzlin::L(TAG).info() << "Pitch bend automation modulation type changed via invokable: " << pitchBendAutomation.toString().toStdString();
328+
}
329+
}
330+
}
331+
259332
} // namespace noteahead

src/application/models/pitch_bend_automations_model.hpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ class PitchBendAutomationsModel : public QAbstractListModel
4141
Pattern,
4242
Track,
4343
Value0,
44-
Value1
44+
Value1,
45+
Modulation_Sine_Cycles,
46+
Modulation_Sine_Amplitude,
47+
Modulation_Sine_Offset,
48+
Modulation_Sine_Inverted,
49+
Modulation_Type
4550
};
4651

4752
PitchBendAutomationsModel();
@@ -62,6 +67,7 @@ class PitchBendAutomationsModel : public QAbstractListModel
6267
Q_INVOKABLE QHash<int, QByteArray> roleNames() const override;
6368

6469
Q_INVOKABLE void applyAll();
70+
Q_INVOKABLE void changeModulationType(int index, int type);
6571

6672
signals:
6773
void pitchBendAutomationChanged(const PitchBendAutomation & PitchBendAutomation);

src/application/service/automation_service.cpp

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ void AutomationService::addMidiCcModulation(quint64 automationId, int type, quin
8383
return automationId == existingAutomation.id();
8484
});
8585
iter != m_automations.midiCc.end()) {
86-
MidiCcAutomation::ModulationParameters modulation { static_cast<MidiCcAutomation::ModulationParameters::ModulationType>(type), static_cast<float>(cycles), amplitude, offset, inverted };
86+
ModulationParameters modulation { static_cast<ModulationParameters::ModulationType>(type), static_cast<float>(cycles), amplitude, offset, inverted };
8787
iter->setModulation(modulation);
8888
notifyChangedLines(*iter);
8989
juzzlin::L(TAG).info() << "MIDI CC Modulation added to automation id " << automationId;
@@ -92,6 +92,21 @@ void AutomationService::addMidiCcModulation(quint64 automationId, int type, quin
9292
}
9393
}
9494

95+
void AutomationService::addPitchBendModulation(quint64 automationId, int type, quint64 cycles, float amplitude, float offset, bool inverted)
96+
{
97+
if (const auto iter = std::ranges::find_if(m_automations.pitchBend, [&](auto && existingAutomation) {
98+
return automationId == existingAutomation.id();
99+
});
100+
iter != m_automations.pitchBend.end()) {
101+
ModulationParameters modulation { static_cast<ModulationParameters::ModulationType>(type), static_cast<float>(cycles), amplitude, offset, inverted };
102+
iter->setModulation(modulation);
103+
notifyChangedLines(*iter);
104+
juzzlin::L(TAG).info() << "Pitch Bend Modulation added to automation id " << automationId;
105+
} else {
106+
juzzlin::L(TAG).error() << "No such automation id to add modulation to: " << automationId;
107+
}
108+
}
109+
95110
void AutomationService::deleteMidiCcAutomation(const MidiCcAutomation & automationToDelete)
96111
{
97112
if (const auto iter = std::ranges::find_if(m_automations.midiCc, [&](auto && existingAutomation) {
@@ -169,7 +184,8 @@ void AutomationService::updatePitchBendAutomation(const PitchBendAutomation & up
169184
if (const auto oldAutomation = *iter; oldAutomation != updatedAutomation) {
170185
*iter = updatedAutomation;
171186
if (oldAutomation.interpolation() != updatedAutomation.interpolation() || //
172-
oldAutomation.enabled() != updatedAutomation.enabled()) {
187+
oldAutomation.enabled() != updatedAutomation.enabled() ||
188+
oldAutomation.modulation() != updatedAutomation.modulation()) {
173189
notifyChangedLinesMerged(oldAutomation, updatedAutomation);
174190
}
175191
juzzlin::L(TAG).info() << "Pitch Bend Automation updated: " << updatedAutomation.toString().toStdString();
@@ -392,9 +408,9 @@ AutomationService::EventList AutomationService::renderMidiCcToEventsByLine(size_
392408
if (modulation.cycles > 0.f || modulation.amplitude > 0.f) {
393409
const double phase = interpolation.line1 > interpolation.line0 ? static_cast<double>(line - interpolation.line0) / (static_cast<double>(interpolation.line1 - interpolation.line0)) : 0;
394410
double modulationValue = 0.0;
395-
if (modulation.type == MidiCcAutomation::ModulationParameters::ModulationType::SineWave) {
411+
if (modulation.type == ModulationParameters::ModulationType::SineWave) {
396412
modulationValue = sineModulationValue(modulation, phase);
397-
} else if (modulation.type == MidiCcAutomation::ModulationParameters::ModulationType::Random) {
413+
} else if (modulation.type == ModulationParameters::ModulationType::Random) {
398414
modulationValue = randomModulationValue(automation.id(), modulation, phase);
399415
}
400416
totalModulation = modulationValue * modulation.amplitude / 100.0; // Amplitude is a percentage
@@ -411,12 +427,12 @@ AutomationService::EventList AutomationService::renderMidiCcToEventsByLine(size_
411427
return events;
412428
}
413429

414-
double AutomationService::sineModulationValue(const MidiCcAutomation::ModulationParameters & modulation, double phase) const
430+
double AutomationService::sineModulationValue(const ModulationParameters & modulation, double phase) const
415431
{
416432
return (modulation.inverted ? -1 : 1) * std::sin(phase * modulation.cycles * 2 * M_PI);
417433
}
418434

419-
double AutomationService::randomModulationValue(size_t automationId, const MidiCcAutomation::ModulationParameters & modulation, double phase) const
435+
double AutomationService::randomModulationValue(size_t automationId, const ModulationParameters & modulation, double phase) const
420436
{
421437
const int sampleIndex = static_cast<int>(std::floor(phase * modulation.cycles));
422438
if (sampleIndex < modulation.cycles) {
@@ -435,14 +451,31 @@ AutomationService::EventList AutomationService::renderPitchBendToEventsByLine(si
435451
if (automation.enabled()) {
436452
const auto & location = automation.location();
437453
const auto & interpolation = automation.interpolation();
454+
const auto & modulation = automation.modulation();
438455
if (location.pattern() == pattern && location.track() == track && location.column() == column && line >= interpolation.line0 && line <= interpolation.line1) {
439456
Interpolator interpolator {
440457
static_cast<size_t>(interpolation.line0),
441458
static_cast<size_t>(interpolation.line1),
442459
static_cast<double>(interpolation.value0),
443460
static_cast<double>(interpolation.value1)
444461
};
445-
const auto percentage = std::clamp(static_cast<int>(interpolator.getValue(static_cast<size_t>(line))), -100, 100);
462+
double interpolatedValue = interpolator.getValue(static_cast<size_t>(line));
463+
464+
double totalModulation = 0.0;
465+
if (modulation.cycles > 0.f || modulation.amplitude > 0.f) {
466+
const double phase = interpolation.line1 > interpolation.line0 ? static_cast<double>(line - interpolation.line0) / (static_cast<double>(interpolation.line1 - interpolation.line0)) : 0;
467+
double modulationValue = 0.0;
468+
if (modulation.type == ModulationParameters::ModulationType::SineWave) {
469+
modulationValue = sineModulationValue(modulation, phase);
470+
} else if (modulation.type == ModulationParameters::ModulationType::Random) {
471+
modulationValue = randomModulationValue(automation.id(), modulation, phase);
472+
}
473+
totalModulation = modulationValue * modulation.amplitude / 100.0; // Amplitude is a percentage
474+
}
475+
totalModulation += modulation.offset / 100.0;
476+
interpolatedValue += interpolatedValue * totalModulation;
477+
478+
const auto percentage = std::clamp(static_cast<int>(std::round(interpolatedValue)), -100, 100);
446479
events.push_back(std::make_shared<Event>(tick, PitchBendData { track, column, static_cast<double>(percentage) }));
447480
}
448481
}
@@ -507,9 +540,9 @@ AutomationService::EventList AutomationService::renderMidiCcToEventsByColumn(siz
507540
if (modulation.cycles > 0.f || modulation.amplitude > 0.f) {
508541
const double phase = interpolation.line1 > interpolation.line0 ? static_cast<double>(line - interpolation.line0) / (static_cast<double>(interpolation.line1 - interpolation.line0)) : 0;
509542
double modulationValue = 0.0;
510-
if (modulation.type == MidiCcAutomation::ModulationParameters::ModulationType::SineWave) {
543+
if (modulation.type == ModulationParameters::ModulationType::SineWave) {
511544
modulationValue = sineModulationValue(modulation, phase);
512-
} else if (modulation.type == MidiCcAutomation::ModulationParameters::ModulationType::Random) {
545+
} else if (modulation.type == ModulationParameters::ModulationType::Random) {
513546
modulationValue = randomModulationValue(automation.id(), modulation, phase);
514547
}
515548
totalModulation = modulationValue * modulation.amplitude / 100.0;
@@ -538,6 +571,7 @@ AutomationService::EventList AutomationService::renderPitchBendToEventsByColumn(
538571
if (automation.enabled()) {
539572
const auto & location = automation.location();
540573
const auto & interpolation = automation.interpolation();
574+
const auto & modulation = automation.modulation();
541575
if (location.pattern() == pattern && location.track() == track && location.column() == column) {
542576
Interpolator interpolator {
543577
static_cast<size_t>(interpolation.line0),
@@ -547,7 +581,23 @@ AutomationService::EventList AutomationService::renderPitchBendToEventsByColumn(
547581
};
548582
std::optional<double> prevValue;
549583
for (size_t line = interpolation.line0; line <= interpolation.line1; line++) {
550-
const auto percentage = std::clamp(static_cast<int>(interpolator.getValue(static_cast<size_t>(line))), -100, 100);
584+
double interpolatedValue = interpolator.getValue(static_cast<size_t>(line));
585+
586+
double totalModulation = 0.0;
587+
if (modulation.cycles > 0.f || modulation.amplitude > 0.f) {
588+
const double phase = interpolation.line1 > interpolation.line0 ? static_cast<double>(line - interpolation.line0) / (static_cast<double>(interpolation.line1 - interpolation.line0)) : 0;
589+
double modulationValue = 0.0;
590+
if (modulation.type == ModulationParameters::ModulationType::SineWave) {
591+
modulationValue = sineModulationValue(modulation, phase);
592+
} else if (modulation.type == ModulationParameters::ModulationType::Random) {
593+
modulationValue = randomModulationValue(automation.id(), modulation, phase);
594+
}
595+
totalModulation = modulationValue * modulation.amplitude / 100.0;
596+
}
597+
totalModulation += modulation.offset / 100.0;
598+
interpolatedValue += interpolatedValue * totalModulation;
599+
600+
const auto percentage = std::clamp(static_cast<int>(std::round(interpolatedValue)), -100, 100);
551601
const double minDiff = 200.0 / 16383;
552602
if (!prevValue || std::fabs(*prevValue - percentage) > minDiff) {
553603
events.push_back(std::make_shared<Event>(tick + line * ticksPerLine, PitchBendData { track, column, static_cast<double>(percentage) }));

0 commit comments

Comments
 (0)