Skip to content

Commit fa61c54

Browse files
committed
Make the Sampler's waveform markers draggable
- The start, the end and the loop point each have a handle now, and neither trim can be dragged past the other - Turning looping on drops the loop point in the middle of the range, where it can be seen and taken hold of - The offsets moved to the amp envelope column, a row each - The picture was read from the file again for every pad setting, and some whole-second offsets read back a second short
1 parent a34a480 commit fa61c54

12 files changed

Lines changed: 339 additions & 90 deletions

CHANGELOG

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ New features:
2121

2222
* Add a loop point to the Sampler's pads
2323
- The pad plays its range once and then repeats only what follows the point
24+
- Turning looping on drops the point in the middle of the range
2425

2526
* Draw the pad's amp envelope over the Sampler's waveform
2627
- White lines over the range, the release included
2728

29+
* Drag the Sampler's start, end and loop markers on the waveform
30+
- Each has a handle, and neither trim can be dragged past the other
31+
2832
* Add choke groups to the Sampler's pads
2933
- Triggering a pad silences the other pads sharing its group
3034
- Eight groups, the way a closed hi-hat cuts off an open one
@@ -35,6 +39,12 @@ Bug fixes:
3539
- A malformed size reported "stoi" instead of saying what was wrong
3640
- A zero or negative size was accepted and saved over the stored one
3741

42+
* Stop rebuilding the Sampler's waveform for every pad setting
43+
- The sample file was read again on every knob move
44+
45+
* Fix the Sampler's offsets at whole seconds
46+
- Some read back a second short, with a thousand milliseconds beside them
47+
3848
Other:
3949

4050
7.3.0

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ set(QML_SOURCE_FILES
6161
${QML_BASE_DIR}/Components/ShortcutHintAction.qml
6262
${QML_BASE_DIR}/Components/SyncSlider.qml
6363
${QML_BASE_DIR}/Components/VirtualKeyboard.qml
64+
${QML_BASE_DIR}/Components/WaveformMarker.qml
6465
${QML_BASE_DIR}/Components/WaveformView.qml
6566
${QML_BASE_DIR}/Constants.qml
6667
${QML_BASE_DIR}/Dialogs/AboutDialog.qml

src/unit_tests/sampler_controller_test/sampler_controller_test.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,58 @@ void SamplerControllerTest::test_selectedPadLoopStart_secondsAndMilliseconds_sho
155155
QVERIFY(std::abs(sampler->sampleLoopStart(SamplerDevice::padStartNote) - 2.25) < 0.001);
156156
}
157157

158+
void SamplerControllerTest::test_selectedPadStartOffset_wholeSecond_shouldReadBackWhole()
159+
{
160+
// An offset is stored as a fraction of a minute in a float, which lands a hair either side of the
161+
// second it was set to. Seventeen lands under, and split by flooring it read as sixteen seconds
162+
// and a thousand milliseconds.
163+
auto reader = std::make_unique<MockAudioFileReader>();
164+
reader->setFrames(static_cast<int64_t>(Constants::defaultSampleRate()) * 20);
165+
const auto sampler = std::make_shared<SamplerDevice>("Test Sampler", std::move(reader));
166+
SamplerController controller { sampler };
167+
controller.setSelectedPad(0);
168+
controller.loadSample(0, "test.wav");
169+
170+
controller.setSelectedPadStartOffsetSeconds(17);
171+
172+
QCOMPARE(controller.selectedPadStartOffsetSeconds(), 17);
173+
QCOMPARE(controller.selectedPadStartOffsetMilliseconds(), 0);
174+
}
175+
176+
void SamplerControllerTest::test_selectedPadLoop_enabled_shouldDropTheLoopPointInTheMiddleOfTheRange()
177+
{
178+
// Four seconds trimmed by a second at each end leaves a two second range, so the point lands one
179+
// second in from where the range begins.
180+
auto reader = std::make_unique<MockAudioFileReader>();
181+
reader->setFrames(static_cast<int64_t>(Constants::defaultSampleRate()) * 4);
182+
const auto sampler = std::make_shared<SamplerDevice>("Test Sampler", std::move(reader));
183+
SamplerController controller { sampler };
184+
controller.setSelectedPad(0);
185+
controller.loadSample(0, "test.wav");
186+
controller.setSelectedPadStartOffsetSeconds(1);
187+
controller.setSelectedPadEndOffsetSeconds(1);
188+
189+
controller.setSelectedPadLoop(true);
190+
191+
QCOMPARE(controller.selectedPadLoopStartSeconds(), 1);
192+
QCOMPARE(controller.selectedPadLoopStartMilliseconds(), 0);
193+
}
194+
195+
void SamplerControllerTest::test_selectedPadLoop_enabled_shouldKeepALoopPointThePadAlreadyHas()
196+
{
197+
auto reader = std::make_unique<MockAudioFileReader>();
198+
reader->setFrames(static_cast<int64_t>(Constants::defaultSampleRate()) * 4);
199+
const auto sampler = std::make_shared<SamplerDevice>("Test Sampler", std::move(reader));
200+
SamplerController controller { sampler };
201+
controller.setSelectedPad(0);
202+
controller.loadSample(0, "test.wav");
203+
controller.setSelectedPadLoopStartSeconds(3);
204+
205+
controller.setSelectedPadLoop(true);
206+
207+
QCOMPARE(controller.selectedPadLoopStartSeconds(), 3);
208+
}
209+
158210
void SamplerControllerTest::test_reset_shouldRestoreDefaultValues()
159211
{
160212
const auto sampler = std::make_shared<SamplerDevice>("Test Sampler");

src/unit_tests/sampler_controller_test/sampler_controller_test.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ private slots:
1616
void test_sampleRateChange_shouldUpdateHzValues();
1717
void test_properties_shouldUpdateDeviceAndEmitSignals();
1818
void test_selectedPadLoopStart_secondsAndMilliseconds_shouldCombineIntoOneOffset();
19+
void test_selectedPadStartOffset_wholeSecond_shouldReadBackWhole();
20+
void test_selectedPadLoop_enabled_shouldDropTheLoopPointInTheMiddleOfTheRange();
21+
void test_selectedPadLoop_enabled_shouldKeepALoopPointThePadAlreadyHas();
1922
void test_reset_shouldRestoreDefaultValues();
2023
void test_setSampler_shouldRefreshGlobalSwitchesToReflectNewInstance();
2124
void test_loadedPads_shouldListOnlyLoadedPads();

src/view/controllers/sampler_controller.cpp

Lines changed: 49 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -179,38 +179,40 @@ void SamplerController::setSelectedPadHpfCutoff(double cutoff)
179179
}
180180
}
181181

182+
SamplerController::OffsetParts SamplerController::splitSeconds(double seconds)
183+
{
184+
// Rounded to whole milliseconds before it is split, not floored: an offset is stored as a
185+
// fraction of a minute in a float, which does not land exactly on the second it was set to, and
186+
// flooring a value a hair short of one reads it as no seconds and a thousand milliseconds.
187+
const auto milliseconds = std::llround(std::max(0.0, seconds) * 1000.0);
188+
return { static_cast<int>(milliseconds / 1000), static_cast<int>(milliseconds % 1000) };
189+
}
190+
182191
int SamplerController::selectedPadStartOffsetSeconds() const
183192
{
184-
if (!m_sampler || m_selectedPad < 0) {
185-
return 0;
186-
}
187-
return static_cast<int>(m_sampler->sampleStartOffset(static_cast<uint8_t>(noteForPad(m_selectedPad))));
193+
const auto note = selectedNote();
194+
return note ? splitSeconds(m_sampler->sampleStartOffset(*note)).seconds : 0;
188195
}
189196

190197
void SamplerController::setSelectedPadStartOffsetSeconds(int seconds)
191198
{
192-
if (m_sampler && m_selectedPad >= 0) {
193-
const double currentOffset = m_sampler->sampleStartOffset(static_cast<uint8_t>(noteForPad(m_selectedPad)));
194-
const double milliseconds = (currentOffset - std::floor(currentOffset)) * 1000.0;
195-
m_sampler->setSampleStartOffset(static_cast<uint8_t>(noteForPad(m_selectedPad)), static_cast<double>(seconds) + milliseconds / 1000.0);
199+
if (const auto note = selectedNote(); note) {
200+
const auto current = splitSeconds(m_sampler->sampleStartOffset(*note));
201+
m_sampler->setSampleStartOffset(*note, seconds + current.milliseconds / 1000.0);
196202
}
197203
}
198204

199205
int SamplerController::selectedPadStartOffsetMilliseconds() const
200206
{
201-
if (!m_sampler || m_selectedPad < 0) {
202-
return 0;
203-
}
204-
const double offset = m_sampler->sampleStartOffset(static_cast<uint8_t>(noteForPad(m_selectedPad)));
205-
return static_cast<int>(std::round((offset - std::floor(offset)) * 1000.0));
207+
const auto note = selectedNote();
208+
return note ? splitSeconds(m_sampler->sampleStartOffset(*note)).milliseconds : 0;
206209
}
207210

208211
void SamplerController::setSelectedPadStartOffsetMilliseconds(int milliseconds)
209212
{
210-
if (m_sampler && m_selectedPad >= 0) {
211-
const double currentOffset = m_sampler->sampleStartOffset(static_cast<uint8_t>(noteForPad(m_selectedPad)));
212-
const double seconds = std::floor(currentOffset);
213-
m_sampler->setSampleStartOffset(static_cast<uint8_t>(noteForPad(m_selectedPad)), seconds + static_cast<double>(milliseconds) / 1000.0);
213+
if (const auto note = selectedNote(); note) {
214+
const auto current = splitSeconds(m_sampler->sampleStartOffset(*note));
215+
m_sampler->setSampleStartOffset(*note, current.seconds + milliseconds / 1000.0);
214216
}
215217
}
216218

@@ -225,83 +227,61 @@ std::optional<uint8_t> SamplerController::selectedNote() const
225227
int SamplerController::selectedPadEndOffsetSeconds() const
226228
{
227229
const auto note = selectedNote();
228-
if (!note) {
229-
return 0;
230-
}
231-
return static_cast<int>(std::floor(m_sampler->sampleEndOffset(*note)));
230+
return note ? splitSeconds(m_sampler->sampleEndOffset(*note)).seconds : 0;
232231
}
233232

234233
void SamplerController::setSelectedPadEndOffsetSeconds(int seconds)
235234
{
236-
const auto note = selectedNote();
237-
if (!note) {
238-
return;
235+
if (const auto note = selectedNote(); note) {
236+
const auto current = splitSeconds(m_sampler->sampleEndOffset(*note));
237+
m_sampler->setSampleEndOffset(*note, seconds + current.milliseconds / 1000.0);
238+
emit selectedPadEndOffsetChanged();
239239
}
240-
const double current = m_sampler->sampleEndOffset(*note);
241-
m_sampler->setSampleEndOffset(*note, static_cast<double>(seconds) + (current - std::floor(current)));
242-
emit selectedPadEndOffsetChanged();
243240
}
244241

245242
int SamplerController::selectedPadEndOffsetMilliseconds() const
246243
{
247244
const auto note = selectedNote();
248-
if (!note) {
249-
return 0;
250-
}
251-
const double offset = m_sampler->sampleEndOffset(*note);
252-
return static_cast<int>(std::round((offset - std::floor(offset)) * 1000.0));
245+
return note ? splitSeconds(m_sampler->sampleEndOffset(*note)).milliseconds : 0;
253246
}
254247

255248
void SamplerController::setSelectedPadEndOffsetMilliseconds(int milliseconds)
256249
{
257-
const auto note = selectedNote();
258-
if (!note) {
259-
return;
250+
if (const auto note = selectedNote(); note) {
251+
const auto current = splitSeconds(m_sampler->sampleEndOffset(*note));
252+
m_sampler->setSampleEndOffset(*note, current.seconds + milliseconds / 1000.0);
253+
emit selectedPadEndOffsetChanged();
260254
}
261-
const double current = m_sampler->sampleEndOffset(*note);
262-
m_sampler->setSampleEndOffset(*note, std::floor(current) + static_cast<double>(milliseconds) / 1000.0);
263-
emit selectedPadEndOffsetChanged();
264255
}
265256

266257
int SamplerController::selectedPadLoopStartSeconds() const
267258
{
268259
const auto note = selectedNote();
269-
if (!note) {
270-
return 0;
271-
}
272-
return static_cast<int>(std::floor(m_sampler->sampleLoopStart(*note)));
260+
return note ? splitSeconds(m_sampler->sampleLoopStart(*note)).seconds : 0;
273261
}
274262

275263
void SamplerController::setSelectedPadLoopStartSeconds(int seconds)
276264
{
277-
const auto note = selectedNote();
278-
if (!note) {
279-
return;
265+
if (const auto note = selectedNote(); note) {
266+
const auto current = splitSeconds(m_sampler->sampleLoopStart(*note));
267+
m_sampler->setSampleLoopStart(*note, seconds + current.milliseconds / 1000.0);
268+
emit selectedPadLoopStartChanged();
280269
}
281-
const double current = m_sampler->sampleLoopStart(*note);
282-
m_sampler->setSampleLoopStart(*note, static_cast<double>(seconds) + (current - std::floor(current)));
283-
emit selectedPadLoopStartChanged();
284270
}
285271

286272
int SamplerController::selectedPadLoopStartMilliseconds() const
287273
{
288274
const auto note = selectedNote();
289-
if (!note) {
290-
return 0;
291-
}
292-
const double offset = m_sampler->sampleLoopStart(*note);
293-
return static_cast<int>(std::round((offset - std::floor(offset)) * 1000.0));
275+
return note ? splitSeconds(m_sampler->sampleLoopStart(*note)).milliseconds : 0;
294276
}
295277

296278
void SamplerController::setSelectedPadLoopStartMilliseconds(int milliseconds)
297279
{
298-
const auto note = selectedNote();
299-
if (!note) {
300-
return;
280+
if (const auto note = selectedNote(); note) {
281+
const auto current = splitSeconds(m_sampler->sampleLoopStart(*note));
282+
m_sampler->setSampleLoopStart(*note, current.seconds + milliseconds / 1000.0);
283+
emit selectedPadLoopStartChanged();
301284
}
302-
const double current = m_sampler->sampleLoopStart(*note);
303-
m_sampler->setSampleLoopStart(*note, std::floor(current) + static_cast<double>(milliseconds) / 1000.0);
304-
emit selectedPadLoopStartChanged();
305285
}
306286

307287
double SamplerController::selectedPadTune() const
@@ -432,6 +412,16 @@ void SamplerController::setSelectedPadLoop(bool loop)
432412
const auto note = selectedNote();
433413
if (note && m_sampler->sampleLoop(*note) != loop) {
434414
m_sampler->setSampleLoop(*note, loop);
415+
// A loop point at the beginning of the range sits underneath the start marker, where it can
416+
// be neither seen nor taken hold of. Turning looping on drops it in the middle of the range
417+
// instead, which is somewhere to drag it from. A point the pad already carries is its own.
418+
if (loop && m_sampler->sampleLoopStart(*note) <= 0.0) {
419+
const auto range = m_sampler->sampleDuration(*note) - m_sampler->sampleStartOffset(*note) - m_sampler->sampleEndOffset(*note);
420+
if (range > 0.0) {
421+
m_sampler->setSampleLoopStart(*note, range / 2.0);
422+
emit selectedPadLoopStartChanged();
423+
}
424+
}
435425
emit selectedPadLoopChanged();
436426
}
437427
}

src/view/controllers/sampler_controller.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ class SamplerController : public DeviceController
194194
void samplerChanged();
195195

196196
private:
197+
//! One offset as the whole-second and millisecond halves the dialog's two spin boxes edit.
198+
struct OffsetParts
199+
{
200+
int seconds = 0;
201+
int milliseconds = 0;
202+
};
203+
204+
static OffsetParts splitSeconds(double seconds);
205+
197206
int noteForPad(int padIndex) const;
198207

199208
//! The note of the selected pad, or nothing when no pad is selected.

0 commit comments

Comments
 (0)