Skip to content

Commit 0b72f9f

Browse files
committed
Merge branch 'pitch-detect'
* pitch-detect: LIB: minor performance optimization for pitch detection LIB: FFT: make test program initialization for new arrays faster GLUI: MorphWavSourceView: fix unused lambda capture warning GLUI: InstEditNote: cleanup pitch detection worker thread initialization LIB: cleanup variable names in pitch detection algorithm GLUI: InstEditNote: update comments for detect note related objects GLUI: InstEditNote: only start pitch detection timer if thread is active LIB: provide a reference to the original pitch detection paper LIB: use static / anon namespace for pitch detection helpers TESTS: .gitignore for testpitchdetect GLUI: InstEditNote: add "Cancel" button for pitch detection GLUI: NoteWidget: provide visual "blinking" feedback for pitch detection GLUI: avoid deep copy of Sample wav_data in pitch detection thread TESTS: testpitchdetect: add tests for WavData -> midi note pitch detection TESTS: testpitchdetect: add unit tests for pitch detect twm LIB: provide pitch detection twm function for unit tests GLUI: do nothing if pitch detection failed GLUI: fix crashes by better InstEditWindow Instrument ownership rules SRC: smenc: handle pitch detection error LIB: detect_pitch: return -1 if pitch detection fails GLUI: display progress bar during pitch detection LIB: report progress from pitch detection function GLUI: support pitch detection in instrument editor in background thread LIB: support stopping pitch detection before it is done (kill_function) LIB: cleanup pitch detection code a little bit LIB: make pitch detection work properly on multi channel input data SRC: smenc: add options to automatically detect fundamental freq / note TESTS: use pitch detection code from library LIB: add pitch detection code to library TESTS: testpitchdetect: optimize freq err -> midi note loop TESTS: testpitchdetect: fix out-of-bounds write due to bad FFT array TESTS: testpitchdetect: avoid full grid search (performance) This could possibly lead to suboptimal results, however for the test material used to validate the correctness of the algorithm, the results are still the very good. TESTS: testpitchdetect: simplify error computation, remove extra pow() TESTS: testpitchdetect: use fast vector sin (performance) TESTS: testpitchdetect: improve accuracy for some cases - use finer grid for initial twm pitch estimate - estimate note as double - ignore outliers in additional note estimation pass TESTS: testpitchdetect: extend search range to 5kHz for fundamental TESTS: testpitchdetect: use all selected partials for error_m2p TESTS: testpitchdetect: use mag sum weighted frequency errors TESTS: testpitchdetect: only use stronger partials for pitch detection TESTS: testpitchdetect: fix error_p2m computation TESTS: testpitchdetect: automatically detect window size to use TESTS: testpitchdetect: try to find most likely midi note from freqs TESTS: testpitchdetect: improve golden section search performance TESTS: testpitchdetect: use golden section search TESTS: testpitchdetect: use local minimum + refinement to find best freq TESTS: testpitchdetect: implement basic two-way-mismatch pitch detection TESTS: testpitchdetect: started to implement pitch detection Signed-off-by: Stefan Westerfeld <stefan@space.twc.de>
2 parents f429904 + b49b54a commit 0b72f9f

File tree

12 files changed

+872
-45
lines changed

12 files changed

+872
-45
lines changed

glui/sminsteditnote.hh

Lines changed: 170 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,81 @@
1313
#include "smscrollview.hh"
1414
#include "smsynthinterface.hh"
1515
#include "smsimplelines.hh"
16+
#include "smpitchdetect.hh"
17+
#include "smprogressbar.hh"
1618

1719
namespace SpectMorph
1820
{
1921

22+
class PitchDetectionThread : public SignalReceiver
23+
{
24+
std::thread worker;
25+
std::atomic<bool> done = false;
26+
std::atomic<bool> killed = false;
27+
std::atomic<double> progress = 0;
28+
double midi_note = -1;
29+
Sample::SharedP sample_shared; // keep Sample wav_data alive as long as needed
30+
Sample *sample = nullptr;
31+
32+
void
33+
on_samples_changed()
34+
{
35+
/* ui thread */
36+
sample = nullptr;
37+
killed = true;
38+
}
39+
public:
40+
PitchDetectionThread (Window *window, Instrument *instrument, Sample *sample) :
41+
sample_shared (sample->shared()),
42+
sample (sample)
43+
{
44+
/* ui thread */
45+
connect (instrument->signal_samples_changed, this, &PitchDetectionThread::on_samples_changed);
46+
47+
worker = std::thread ([this]()
48+
{
49+
/* worker thread
50+
*
51+
* using sample_shared keeps a reference on the wav_data of the sample,
52+
* which keeps alive the wav_data we're using for pitch detection even
53+
* if the Instrument Sample is destroyed before we're done
54+
*/
55+
midi_note = detect_pitch (sample_shared->wav_data(), [this] (double progress) { this->progress = progress; return killed.load(); });
56+
done = true;
57+
});
58+
}
59+
~PitchDetectionThread()
60+
{
61+
/* ui thread */
62+
killed = true;
63+
worker.join();
64+
}
65+
int
66+
result()
67+
{
68+
/* ui thread */
69+
assert (is_done());
70+
return lrint (midi_note);
71+
}
72+
double
73+
timer_tick()
74+
{
75+
/* ui thread */
76+
if (is_done())
77+
{
78+
if (sample && midi_note != -1) // midi_note is -1 if pitch detection failed
79+
sample->set_midi_note (lrint (midi_note));
80+
}
81+
return progress.load();
82+
}
83+
bool
84+
is_done() const
85+
{
86+
/* any thread */
87+
return done.load();
88+
}
89+
};
90+
2091
class NoteWidget : public Widget
2192
{
2293
int first = 12;
@@ -49,6 +120,9 @@ class NoteWidget : public Widget
49120
int mouse_note = -1;
50121
int left_pressed_note = -1;
51122
int right_pressed_note = -1;
123+
int pitch_detection_note = -1;
124+
int pitch_detection_age = 0;
125+
Timer *pitch_detection_note_timer = nullptr;
52126
Instrument *instrument = nullptr;
53127
SynthInterface *synth_interface = nullptr;
54128
std::vector<int> active_notes;
@@ -59,6 +133,8 @@ public:
59133
instrument (instrument),
60134
synth_interface (synth_interface)
61135
{
136+
pitch_detection_note_timer = new Timer (this);
137+
connect (pitch_detection_note_timer->signal_timeout, this, &NoteWidget::on_pitch_detection_timer);
62138
connect (instrument->signal_samples_changed, this, &NoteWidget::on_samples_changed);
63139
}
64140
void
@@ -126,7 +202,8 @@ public:
126202
for (auto note : active_notes)
127203
if (n == note)
128204
note_playing = true;
129-
if (n == mouse_note || note_playing)
205+
bool display_pitch_detect = (n == pitch_detection_note && (pitch_detection_age % 2) == 0);
206+
if (n == mouse_note || display_pitch_detect || note_playing)
130207
{
131208
double xspace = width() / cols / 10;
132209
double yspace = height() / rows / 10;
@@ -138,6 +215,12 @@ public:
138215
fill_color = ThemeColor::SLIDER;
139216
text_color = Color (0, 0, 0);
140217
}
218+
else if (display_pitch_detect)
219+
{
220+
frame_color = Color::null();
221+
fill_color = ThemeColor::MENU_ITEM;
222+
text_color = Color (0, 0, 0);
223+
}
141224
else
142225
{
143226
frame_color = Color (0.8, 0.8, 0.8);
@@ -236,14 +319,36 @@ public:
236319
update();
237320
}
238321
}
322+
void
323+
set_pitch_detection_note (int note)
324+
{
325+
pitch_detection_age = 0;
326+
pitch_detection_note = note;
327+
update();
328+
329+
pitch_detection_note_timer->start (300);
330+
}
331+
void
332+
on_pitch_detection_timer()
333+
{
334+
pitch_detection_age++;
335+
if (pitch_detection_age == 3)
336+
pitch_detection_note_timer->stop();
337+
update();
338+
}
239339
};
240340

241341
class InstEditNote : public Window
242342
{
243343
NoteWidget *note_widget = nullptr;
344+
Timer *detect_note_timer = nullptr;
345+
ProgressBar *detect_note_progress_bar = nullptr;
346+
Button *detect_note_button = nullptr;
347+
Button *detect_note_cancel_button = nullptr;
348+
std::unique_ptr<PitchDetectionThread> pitch_detection_thread;
244349
public:
245350
InstEditNote (Window *window, Instrument *instrument, SynthInterface *synth_interface) :
246-
Window (*window->event_loop(), "SpectMorph - Instrument Note", 13 * 40 + 2 * 8, 9 * 40 + 6 * 8, 0, false, window->native_window())
351+
Window (*window->event_loop(), "SpectMorph - Instrument Note", 13 * 40 + 2 * 8, 9 * 40 + 8 * 8, 0, false, window->native_window())
247352
{
248353
set_close_callback ([this]() {
249354
signal_closed();
@@ -255,7 +360,7 @@ public:
255360

256361
note_widget = new NoteWidget (this, instrument, synth_interface);
257362
FixedGrid grid;
258-
grid.add_widget (note_widget, 1, 1, width() / 8 - 2, height() / 8 - 6);
363+
grid.add_widget (note_widget, 1, 1, width() / 8 - 2, height() / 8 - 8);
259364

260365
Label *left = new Label (this, "Left Click");
261366
Label *left_txt = new Label (this, "Play Reference");
@@ -273,19 +378,74 @@ public:
273378
Label *space_txt = new Label (this, "Play Selected Note");
274379
space->set_bold (true);
275380

381+
/*--- detect note: timer ---*/
382+
detect_note_timer = new Timer (this);
383+
connect (detect_note_timer->signal_timeout, [this] ()
384+
{
385+
if (pitch_detection_thread)
386+
{
387+
double progress = pitch_detection_thread->timer_tick();
388+
detect_note_progress_bar->set_value (progress * 0.01);
389+
if (pitch_detection_thread->is_done())
390+
{
391+
int note = pitch_detection_thread->result();
392+
if (note >= 0)
393+
note_widget->set_pitch_detection_note (note);
394+
pitch_detection_thread.reset();
395+
detect_note_button->set_visible (true);
396+
detect_note_progress_bar->set_visible (false);
397+
detect_note_cancel_button->set_visible (false);
398+
}
399+
}
400+
else
401+
{
402+
detect_note_timer->stop();
403+
}
404+
});
405+
406+
/*--- detect note: button ---*/
407+
detect_note_button = new Button (this, "Detect Midi Note");
408+
connect (detect_note_button->signal_clicked, [this, instrument]()
409+
{
410+
Sample *sample = instrument->sample (instrument->selected());
411+
if (sample)
412+
{
413+
pitch_detection_thread = std::make_unique<PitchDetectionThread> (this, instrument, sample);
414+
detect_note_timer->start (0);
415+
detect_note_button->set_visible (false);
416+
detect_note_progress_bar->set_visible (true);
417+
detect_note_cancel_button->set_visible (true);
418+
}
419+
});
420+
/*--- detect note: cancel button ---*/
421+
detect_note_cancel_button = new Button (this, "Cancel");
422+
connect (detect_note_cancel_button->signal_clicked, [this]()
423+
{
424+
pitch_detection_thread.reset();
425+
detect_note_button->set_visible (true);
426+
detect_note_progress_bar->set_visible (false);
427+
detect_note_cancel_button->set_visible (false);
428+
});
429+
detect_note_cancel_button->set_visible (false);
430+
431+
/*--- detect note: progress ---*/
432+
detect_note_progress_bar = new ProgressBar (this);
433+
detect_note_progress_bar->set_visible (false);
434+
276435
grid.dx = 4;
277-
grid.dy = height() / 8 - 5;
436+
grid.dy = height() / 8 - 7;
278437

279438
double xw = 12;
280-
grid.add_widget (dbl, 0, 2, xw, 3);
281-
grid.add_widget (dbl_txt, xw, 2, 20, 3);
282439
grid.add_widget (space, 0, 0, xw, 3);
283440
grid.add_widget (space_txt, xw, 0, 20, 3);
441+
grid.add_widget (detect_note_button, 0, 3, 25, 3);
442+
grid.add_widget (detect_note_progress_bar, 0, 3, 16, 3);
443+
grid.add_widget (detect_note_cancel_button, 17, 3, 8, 3);
284444

285445
grid.dx = width() / 8 / 2;
286-
grid.dy = height() / 8 - 5;
446+
grid.dy = height() / 8 - 7;
287447

288-
grid.add_widget (new VLine (this, Color (0.6, 0.6, 0.6), 2), 0, 0, 1, 5);
448+
grid.add_widget (new VLine (this, Color (0.6, 0.6, 0.6), 2), 0, 0, 1, 7);
289449

290450
grid.dx += 4;
291451

@@ -294,6 +454,8 @@ public:
294454
grid.add_widget (left_txt, xw, 0, 20, 3);
295455
grid.add_widget (right, 0, 2, xw, 3);
296456
grid.add_widget (right_txt, xw, 2, 20, 3);
457+
grid.add_widget (dbl, 0, 4, xw, 3);
458+
grid.add_widget (dbl_txt, xw, 4, 20, 3);
297459

298460
show();
299461
}

glui/sminsteditwindow.cc

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ InstEditBackend::InstEditBackend (SynthInterface *synth_interface) :
8383
synth_interface (synth_interface),
8484
cache_group (InstEncCache::the()->create_group())
8585
{
86+
synth_interface->synth_inst_edit_update (true, nullptr, nullptr);
87+
}
88+
89+
InstEditBackend::~InstEditBackend()
90+
{
91+
synth_interface->synth_inst_edit_update (false, nullptr, nullptr);
8692
}
8793

8894
void
@@ -138,13 +144,13 @@ InstEditBackend::on_timer()
138144

139145
// ---------------- InstEditWindow ----------------
140146
//
141-
InstEditWindow::InstEditWindow (EventLoop& event_loop, Instrument *edit_instrument, SynthInterface *synth_interface, Window *parent_window) :
147+
InstEditWindow::InstEditWindow (EventLoop& event_loop, const Instrument *edit_instrument, SynthInterface *synth_interface, Window *parent_window) :
142148
Window (event_loop, "SpectMorph - Instrument Editor", win_width, win_height, 0, false, parent_window ? parent_window->native_window() : 0),
143149
m_backend (synth_interface),
144150
synth_interface (synth_interface)
145151
{
146152
assert (edit_instrument != nullptr);
147-
instrument = edit_instrument;
153+
instrument = edit_instrument->clone();
148154

149155
/* make a backup to be able to revert */
150156
ZipWriter writer;
@@ -411,6 +417,9 @@ InstEditWindow::~InstEditWindow()
411417
delete inst_edit_volume;
412418
inst_edit_volume = nullptr;
413419
}
420+
421+
assert (instrument);
422+
delete instrument;
414423
}
415424

416425
void
@@ -480,9 +489,6 @@ InstEditWindow::load_sample_convert_from_stereo (const WavData& wav_data, const
480489
void
481490
InstEditWindow::on_synth_notify_event (SynthNotifyEvent *notify_event)
482491
{
483-
if (!instrument)
484-
return;
485-
486492
auto iev = dynamic_cast<InstEditVoiceEvent *> (notify_event);
487493
if (!iev)
488494
return;
@@ -556,9 +562,6 @@ InstEditWindow::on_synth_notify_event (SynthNotifyEvent *notify_event)
556562
void
557563
InstEditWindow::on_selected_sample_changed()
558564
{
559-
if (!instrument) // during close we cannot access the instrument anymore
560-
return;
561-
562565
sample_combobox->clear();
563566
if (instrument->size() == 0)
564567
{
@@ -604,9 +607,6 @@ InstEditWindow::on_selected_sample_changed()
604607
void
605608
InstEditWindow::on_samples_changed()
606609
{
607-
if (!instrument) // during close we cannot access the instrument anymore
608-
return;
609-
610610
on_selected_sample_changed();
611611
m_backend.update_instrument (instrument, reference);
612612
}
@@ -685,9 +685,6 @@ InstEditWindow::update_auto_checkboxes()
685685
void
686686
InstEditWindow::on_global_changed()
687687
{
688-
if (!instrument) // during close we cannot access the instrument anymore
689-
return;
690-
691688
update_auto_checkboxes();
692689

693690
name_line_edit->set_text (instrument->name());
@@ -1018,14 +1015,23 @@ InstEditWindow::set_playing (bool new_playing)
10181015
play_button->set_icon (playing ? IconButton::STOP : IconButton::PLAY);
10191016
}
10201017

1021-
void
1022-
InstEditWindow::clear_edit_instrument()
1018+
/*
1019+
* Instrument ownership:
1020+
* - in our constructor we make a deep copy of the instrument we want to edit
1021+
* so we can modify it and use its signals without any interference
1022+
* - in this function we return a deep copy of the edited instruments to
1023+
* ensure that the caller can modify, keep or destroy the Instrument at any
1024+
* time
1025+
* - in our destructor, we delete our copy of the instrument
1026+
*
1027+
* This ensures that draw events or timer events that happen after the
1028+
* window has been closed (but before the InstEditWindow has been destroyed)
1029+
* can always access our own private version of the Instrument.
1030+
*/
1031+
std::unique_ptr<Instrument>
1032+
InstEditWindow::get_modified_instrument()
10231033
{
1024-
/* this is called if the window is closed: after the close event
1025-
* we should no longer access the instrument because it could be
1026-
* deleted already
1027-
*/
1028-
instrument = nullptr;
1034+
return std::unique_ptr<Instrument> (instrument->clone());
10291035
}
10301036

10311037
void

glui/sminsteditwindow.hh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class InstEditBackend
5050

5151
public:
5252
InstEditBackend (SynthInterface *synth_interface);
53+
~InstEditBackend();
5354

5455
void update_instrument (const Instrument *instrument, const std::string& reference);
5556
bool have_builder();
@@ -123,10 +124,10 @@ public:
123124
static const int win_width = 744;
124125
static const int win_height = 560;
125126

126-
InstEditWindow (EventLoop& event_loop, Instrument *edit_instrument, SynthInterface *synth_interface, Window *parent_window = nullptr);
127+
InstEditWindow (EventLoop& event_loop, const Instrument *edit_instrument, SynthInterface *synth_interface, Window *parent_window = nullptr);
127128
~InstEditWindow();
128129

129-
void clear_edit_instrument();
130+
std::unique_ptr<Instrument> get_modified_instrument();
130131

131132
bool auto_select() const;
132133
void set_auto_select (bool auto_select);

0 commit comments

Comments
 (0)