From f45474e68831f3724457a0fb9d9d83b3d64408e0 Mon Sep 17 00:00:00 2001 From: Sampo Savolainen Date: Tue, 4 Mar 2008 17:56:17 +0000 Subject: [PATCH] Improvements on the FFT analysis tool - Enable FFT analysis by default - FFT graph is now in cairo - The window is now a window instead of a dialog - Analysis window can be resized - The view can be switched between normalized and an absolute value - The minimum and maximum values for a graph can be shown git-svn-id: svn://localhost/ardour2/branches/2.0-ongoing@3135 d708f5d6-7413-0410-9779-e7cbd77b26cf --- SConstruct | 4 +- gtk2_ardour/analysis_window.cc | 49 ++++++-- gtk2_ardour/analysis_window.h | 20 ++- gtk2_ardour/fft_graph.cc | 222 ++++++++++++++++++++++----------- gtk2_ardour/fft_graph.h | 18 ++- gtk2_ardour/fft_result.cc | 89 ++++++++++--- gtk2_ardour/fft_result.h | 11 +- 7 files changed, 302 insertions(+), 111 deletions(-) diff --git a/SConstruct b/SConstruct index b68a45d621..804fa6bf91 100644 --- a/SConstruct +++ b/SConstruct @@ -38,7 +38,7 @@ opts.AddOptions( EnumOption('DIST_TARGET', 'Build target for cross compiling packagers', 'auto', allowed_values=('auto', 'i386', 'i686', 'x86_64', 'powerpc', 'tiger', 'panther', 'leopard', 'none' ), ignorecase=2), BoolOption('DMALLOC', 'Compile and link using the dmalloc library', 0), BoolOption('EXTRA_WARN', 'Compile with -Wextra, -ansi, and -pedantic. Might break compilation. For pedants', 0), - BoolOption('FFT_ANALYSIS', 'Include FFT analysis window', 0), + BoolOption('FFT_ANALYSIS', 'Include FFT analysis window', 1), BoolOption('FPU_OPTIMIZATION', 'Build runtime checked assembler code', 1), BoolOption('LIBLO', 'Compile with support for liblo library', 1), BoolOption('NLS', 'Set to turn on i18n support', 1), @@ -527,7 +527,7 @@ if env['FFT_ANALYSIS']: conf = Configure(libraries['fftw3']) if conf.CheckHeader ('fftw3.h') == False: - print ('FFT Analysis cannot be compiled without the FFTW3 headers, which do not seem to be installed') + print ('Ardour cannot be compiled without the FFTW3 headers, which do not seem to be installed') sys.exit (1) conf.Finish() diff --git a/gtk2_ardour/analysis_window.cc b/gtk2_ardour/analysis_window.cc index 957dde3d1f..ae557f4c10 100644 --- a/gtk2_ardour/analysis_window.cc +++ b/gtk2_ardour/analysis_window.cc @@ -41,8 +41,10 @@ using namespace ARDOUR; using namespace PBD; -AnalysisWindow::AnalysisWindow() - : ArdourDialog(_("analysis window")), +AnalysisWindow::AnalysisWindow() : + + show_minmax_button (_("Show frequency power range")), + show_normalized_button (_("Normalize values")), source_selection_label (_("Signal source")), source_selection_ranges_rb (_("Selected ranges")), @@ -52,8 +54,11 @@ AnalysisWindow::AnalysisWindow() display_model_composite_separate_rb (_("Composite graphs for each track")), display_model_composite_all_tracks_rb (_("Composite graph of all tracks")), - fft_graph (2048) + fft_graph (16384) { + set_name(_("FFT analysis window")); + set_title(_("FFT analysis window")); + track_list_ready = false; // Left side: track list + controls @@ -124,17 +129,31 @@ AnalysisWindow::AnalysisWindow() bind ( mem_fun(*this, &AnalysisWindow::display_model_changed), &display_model_composite_all_tracks_rb)); } - vbox.pack_start(hseparator2, false, false); + // Analyze button refresh_button.set_name("EditorGTKButton"); - refresh_button.set_label(_("Analyze data")); + refresh_button.set_label(_("Re-analyze data")); refresh_button.signal_clicked().connect ( bind ( mem_fun(*this, &AnalysisWindow::analyze_data), &refresh_button)); vbox.pack_start(refresh_button, false, false, 10); + + + // Feature checkboxes + + // minmax + show_minmax_button.signal_toggled().connect( mem_fun(*this, &AnalysisWindow::show_minmax_changed)); + vbox.pack_start(show_minmax_button, false, false); + + // normalize + show_normalized_button.signal_toggled().connect( mem_fun(*this, &AnalysisWindow::show_normalized_changed)); + vbox.pack_start(show_normalized_button, false, false); + + + - hbox.pack_start(vbox); + hbox.pack_start(vbox, Gtk::PACK_SHRINK); // Analysis window on the right fft_graph.ensure_style(); @@ -144,11 +163,9 @@ AnalysisWindow::AnalysisWindow() // And last we pack the hbox - get_vbox()->pack_start(hbox); - + add(hbox); + show_all(); track_list.show_all(); - - get_vbox()->show_all(); } AnalysisWindow::~AnalysisWindow() @@ -156,6 +173,18 @@ AnalysisWindow::~AnalysisWindow() } +void +AnalysisWindow::show_minmax_changed() +{ + fft_graph.set_show_minmax(show_minmax_button.get_active()); +} + +void +AnalysisWindow::show_normalized_changed() +{ + fft_graph.set_show_normalized(show_normalized_button.get_active()); +} + void AnalysisWindow::set_rangemode() { diff --git a/gtk2_ardour/analysis_window.h b/gtk2_ardour/analysis_window.h index cd1243bb6a..45752c4a34 100644 --- a/gtk2_ardour/analysis_window.h +++ b/gtk2_ardour/analysis_window.h @@ -31,6 +31,7 @@ #include #include #include +#include #include @@ -42,7 +43,7 @@ #include "fft_result.h" -class AnalysisWindow : public ArdourDialog +class AnalysisWindow : public Gtk::Window { public: AnalysisWindow (); @@ -55,12 +56,18 @@ class AnalysisWindow : public ArdourDialog void analyze (); + const void set_session(ARDOUR::Session *session) { _session = session; }; + private: + ARDOUR::Session *_session; + void clear_tracklist(); void source_selection_changed (Gtk::RadioButton *); void display_model_changed (Gtk::RadioButton *); + void show_minmax_changed (); + void show_normalized_changed (); void analyze_data (Gtk::Button *); @@ -88,7 +95,8 @@ class AnalysisWindow : public ArdourDialog Gtk::TreeView track_list; Gtk::Label source_selection_label; - + + Gtk::RadioButton source_selection_ranges_rb; Gtk::RadioButton source_selection_regions_rb; @@ -98,9 +106,13 @@ class AnalysisWindow : public ArdourDialog Gtk::RadioButton display_model_composite_separate_rb; Gtk::RadioButton display_model_composite_all_tracks_rb; - Gtk::HSeparator hseparator2; - Gtk::Button refresh_button; + + + Gtk::CheckButton show_minmax_button; + Gtk::CheckButton show_normalized_button; + + // The graph FFTGraph fft_graph; diff --git a/gtk2_ardour/fft_graph.cc b/gtk2_ardour/fft_graph.cc index e7a0fd75b6..077c1e162c 100644 --- a/gtk2_ardour/fft_graph.cc +++ b/gtk2_ardour/fft_graph.cc @@ -51,6 +51,9 @@ FFTGraph::FFTGraph(int windowSize) _a_window = 0; + _show_minmax = false; + _show_normalized = false; + setWindowSize(windowSize); } @@ -151,23 +154,6 @@ FFTGraph::prepareResult(Gdk::Color color, string trackname) return res; } -void -FFTGraph::analyze(float *window, float *composite) -{ - int i; - // Copy the data and apply the hanning window - for (i = 0; i < _windowSize; i++) { - _in[i] = window[ i ] * _hanning[ i ]; - } - - fftwf_execute(_plan); - - composite[0] += (_out[0] * _out[0]); - - for (i=1; i < _dataSize - 1; i++) { // TODO: check with Jesse whether this is really correct - composite[i] += (_out[i] * _out[i]) + (_out[_windowSize-i] * _out[_windowSize-i]); - } -} void FFTGraph::set_analysis_window(AnalysisWindow *a_window) @@ -198,17 +184,18 @@ FFTGraph::draw_scales(Glib::RefPtr window) window->draw_line(white, h_margin, v_margin, h_margin, height - v_margin ); // Line 2 - window->draw_line(white, width - h_margin, v_margin, width - h_margin, height - v_margin ); + window->draw_line(white, width - h_margin + 1, v_margin, width - h_margin + 1, height - v_margin ); // Line 3 window->draw_line(white, h_margin, height - v_margin, width - h_margin, height - v_margin ); #define DB_METRIC_LENGTH 8 - // Line 5 + // Line 4 window->draw_line(white, h_margin - DB_METRIC_LENGTH, v_margin, h_margin, v_margin ); - // Line 6 - window->draw_line(white, width - h_margin, v_margin, width - h_margin + DB_METRIC_LENGTH, v_margin ); + // Line 5 + window->draw_line(white, width - h_margin + 1, v_margin, width - h_margin + DB_METRIC_LENGTH, v_margin ); + if (graph_gc == 0) { @@ -229,8 +216,24 @@ FFTGraph::draw_scales(Glib::RefPtr window) // Draw logscale int logscale_pos = 0; int position_on_scale; + + +/* TODO, write better scales and change the log function so that octaves are of equal pixel length + float scale_points[10] = { 55.0, 110.0, 220.0, 440.0, 880.0, 1760.0, 3520.0, 7040.0, 14080.0, 28160.0 }; + + for (int x = 0; x < 10; x++) { + + // i = 0.. _dataSize-1 + float freq_at_bin = (SR/2.0) * ((double)i / (double)_dataSize); + + + + freq_at_pixel = FFT_START * exp( FFT_RANGE * pixel / (double)(currentScaleWidth - 1) ); + } + */ + for (int x = 1; x < 8; x++) { - position_on_scale = (int)floor( (double)scaleWidth*(double)x/8.0); + position_on_scale = (int)floor( (double)currentScaleWidth*(double)x/8.0); while (_logScale[logscale_pos] < position_on_scale) logscale_pos++; @@ -251,7 +254,7 @@ FFTGraph::draw_scales(Glib::RefPtr window) layout->set_text(label); - window->draw_line(graph_gc, coord, v_margin, coord, height - v_margin); + window->draw_line(graph_gc, coord, v_margin, coord, height - v_margin - 1); int width, height; layout->get_pixel_size (width, height); @@ -268,12 +271,19 @@ FFTGraph::redraw() Glib::Mutex::Lock lm (_a_window->track_list_lock); draw_scales(get_window()); + if (_a_window == 0) return; if (!_a_window->track_list_ready) return; + + cairo_t *cr; + cr = gdk_cairo_create(GDK_DRAWABLE(get_window()->gobj())); + cairo_set_line_width(cr, 1.5); + cairo_translate(cr, (float)v_margin + 1.0, (float)h_margin); + // Find "session wide" min & max @@ -300,17 +310,24 @@ FFTGraph::redraw() max = res->maximum(); } } - - int graph_height = height - 2 * h_margin; - if (graph_gc == 0) { - graph_gc = GC::create( get_window() ); + if (!_show_normalized) { + min = -150.0f; + max = 0.0f; } - - double pixels_per_db = (double)graph_height / (double)(max - min); + //int graph_height = height - 2 * h_margin; + + float fft_pane_size_w = (float)(width - 2*v_margin) - 1.0; + float fft_pane_size_h = (float)(height - 2*h_margin); + + double pixels_per_db = (double)fft_pane_size_h / (double)(max - min); + + cairo_rectangle(cr, 0.0, 0.0, fft_pane_size_w, fft_pane_size_h); + cairo_clip(cr); + for (TreeIter i = track_rows.begin(); i != track_rows.end(); i++) { TreeModel::Row row = *i; @@ -326,72 +343,104 @@ FFTGraph::redraw() if (res->minimum() == res->maximum()) { continue; } + + float mpp; - std::string name = row[_a_window->tlcols.trackname]; + if (_show_minmax) { + mpp = -1000000.0; + + cairo_set_source_rgba(cr, res->get_color().get_red_p(), res->get_color().get_green_p(), res->get_color().get_blue_p(), 0.30); + cairo_move_to(cr, 0.5f + (float)_logScale[0], 0.5f + (float)( fft_pane_size_h - (int)floor( (res->maxAt(0) - min) * pixels_per_db) )); + + // Draw the line of maximum values + for (int x = 1; x < res->length(); x++) { + if (res->maxAt(x) > mpp) + mpp = res->maxAt(x); + mpp = fmax(mpp, min); + mpp = fmin(mpp, max); + + // If the next point on the log scale is at the same location, + // don't draw yet + if (x + 1 < res->length() && _logScale[x] == _logScale[x + 1]) { + continue; + } + + float X = 0.5f + (float)_logScale[x]; + float Y = 0.5f + (float)( fft_pane_size_h - (int)floor( (mpp - min) * pixels_per_db) ); + + cairo_line_to(cr, X, Y); + + mpp = -1000000.0; + } + + mpp = +10000000.0; + // Draw back to the start using the minimum value + for (int x = res->length()-1; x >= 0; x--) { + if (res->minAt(x) < mpp) + mpp = res->minAt(x); + mpp = fmax(mpp, min); + mpp = fmin(mpp, max); + + // If the next point on the log scale is at the same location, + // don't draw yet + if (x - 1 > 0 && _logScale[x] == _logScale[x - 1]) { + continue; + } + + float X = 0.5f + (float)_logScale[x]; + float Y = 0.5f + (float)( fft_pane_size_h - (int)floor( (mpp - min) * pixels_per_db) ); + + cairo_line_to(cr, X, Y ); + + mpp = +10000000.0; + } + + cairo_close_path(cr); + + cairo_fill(cr); + } + + // Set color from track - graph_gc->set_rgb_fg_color( res->get_color() ); + cairo_set_source_rgb(cr, res->get_color().get_red_p(), res->get_color().get_green_p(), res->get_color().get_blue_p()); - float mpp = -1000000.0; - int prevx = 0; - float prevSample = min; - - for (int x = 0; x < res->length() - 1; x++) { + mpp = -1000000.0; + + cairo_move_to(cr, 0.5, fft_pane_size_h-0.5); + + for (int x = 0; x < res->length(); x++) { - if (res->sampleAt(x) > mpp) - mpp = res->sampleAt(x); + + if (res->avgAt(x) > mpp) + mpp = res->avgAt(x); + mpp = fmax(mpp, min); + mpp = fmin(mpp, max); // If the next point on the log scale is at the same location, // don't draw yet - if (x + 1 < res->length() && - _logScale[x] == _logScale[x + 1]) { + if (x + 1 < res->length() && _logScale[x] == _logScale[x + 1]) { continue; } - get_window()->draw_line( - graph_gc, - v_margin + 1 + prevx, - graph_height - (int)floor( (prevSample - min) * pixels_per_db) + h_margin - 1, - v_margin + 1 + _logScale[x], - graph_height - (int)floor( (mpp - min) * pixels_per_db) + h_margin - 1); - - prevx = _logScale[x]; - prevSample = mpp; - + cairo_line_to(cr, 0.5f + (float)_logScale[x], 0.5f + (float)( fft_pane_size_h - (int)floor( (mpp - min) * pixels_per_db) )); mpp = -1000000.0; - } + + cairo_stroke(cr); } + cairo_destroy(cr); } void FFTGraph::on_size_request(Gtk::Requisition* requisition) { - width = scaleWidth + h_margin * 2; - height = scaleHeight + 2 + v_margin * 2; + width = max(requisition->width, minScaleWidth + h_margin * 2); + height = max(requisition->height, minScaleHeight + 2 + v_margin * 2); - if (_logScale != 0) { - free(_logScale); - } - _logScale = (int *) malloc(sizeof(int) * _dataSize); - - float SR = 44100; - float FFT_START = SR/(double)_dataSize; - float FFT_END = SR/2.0; - float FFT_RANGE = log( FFT_END / FFT_START); - float pixel = 0; - for (int i = 0; i < _dataSize; i++) { - float freq_at_bin = (SR/2.0) * ((double)i / (double)_dataSize); - float freq_at_pixel = FFT_START * exp( FFT_RANGE * pixel / (double)scaleWidth ); - while (freq_at_bin > freq_at_pixel) { - pixel++; - freq_at_pixel = FFT_START * exp( FFT_RANGE * pixel / (double)scaleWidth ); - } - _logScale[i] = (int)floor(pixel); -//printf("logscale at %d = %3.3f, freq_at_pixel %3.3f, freq_at_bin %3.3f, scaleWidth %d\n", i, pixel, freq_at_pixel, freq_at_bin, scaleWidth); - } + update_size(); requisition->width = width;; requisition->height = height; @@ -403,7 +452,32 @@ FFTGraph::on_size_allocate(Gtk::Allocation & alloc) width = alloc.get_width(); height = alloc.get_height(); - DrawingArea::on_size_allocate (alloc); + update_size(); + DrawingArea::on_size_allocate (alloc); +} + +void +FFTGraph::update_size() +{ + currentScaleWidth = width - h_margin*2; + currentScaleHeight = height - 2 - v_margin*2; + + float SR = 44100; + float FFT_START = SR/(double)_dataSize; + float FFT_END = SR/2.0; + float FFT_RANGE = log( FFT_END / FFT_START); + float pixel = 0; + for (int i = 0; i < _dataSize; i++) { + float freq_at_bin = (SR/2.0) * ((double)i / (double)_dataSize); + float freq_at_pixel; + pixel--; + do { + pixel++; + freq_at_pixel = FFT_START * exp( FFT_RANGE * pixel / (double)(currentScaleWidth - 1) ); + } while (freq_at_bin > freq_at_pixel); + + _logScale[i] = (int)floor(pixel); + } } diff --git a/gtk2_ardour/fft_graph.h b/gtk2_ardour/fft_graph.h index 73636b989d..bbf7774741 100644 --- a/gtk2_ardour/fft_graph.h +++ b/gtk2_ardour/fft_graph.h @@ -54,27 +54,34 @@ class FFTGraph : public Gtk::DrawingArea void on_size_allocate(Gtk::Allocation & alloc); FFTResult *prepareResult(Gdk::Color color, std::string trackname); + const void set_show_minmax (bool v) { _show_minmax = v; redraw(); }; + const void set_show_normalized (bool v) { _show_normalized = v; redraw(); }; + private: + void update_size(); + void setWindowSize_internal(int windowSize); void draw_scales(Glib::RefPtr window); - static const int scaleWidth = 512; - static const int scaleHeight = 420; + static const int minScaleWidth = 512; + static const int minScaleHeight = 420; + + int currentScaleWidth; + int currentScaleHeight; static const int h_margin = 20; static const int v_margin = 20; + Glib::RefPtr graph_gc; int width; int height; - void analyze(float *window, float *composite); int _windowSize; int _dataSize; Glib::RefPtr layout; - Glib::RefPtr graph_gc; AnalysisWindow *_a_window; fftwf_plan _plan; @@ -84,6 +91,9 @@ class FFTGraph : public Gtk::DrawingArea float *_hanning; int *_logScale; + bool _show_minmax; + bool _show_normalized; + friend class FFTResult; }; diff --git a/gtk2_ardour/fft_result.cc b/gtk2_ardour/fft_result.cc index f5acef92ed..d692b9152b 100644 --- a/gtk2_ardour/fft_result.cc +++ b/gtk2_ardour/fft_result.cc @@ -37,8 +37,16 @@ FFTResult::FFTResult(FFTGraph *graph, Gdk::Color color, string trackname) _averages = 0; - _data = (float *) malloc(sizeof(float) * _dataSize); - memset(_data,0,sizeof(float) * _dataSize); + _data_avg = (float *) malloc(sizeof(float) * _dataSize); + memset(_data_avg,0,sizeof(float) * _dataSize); + + _data_min = (float *) malloc(sizeof(float) * _dataSize); + _data_max = (float *) malloc(sizeof(float) * _dataSize); + + for (int i = 0; i < _dataSize; i++) { + _data_min[i] = FLT_MAX; + _data_max[i] = FLT_MIN; + } _color = color; _trackname = trackname; @@ -47,7 +55,34 @@ FFTResult::FFTResult(FFTGraph *graph, Gdk::Color color, string trackname) void FFTResult::analyzeWindow(float *window) { - _graph->analyze(window, _data); + float *_hanning = _graph->_hanning; + float *_in = _graph->_in; + float *_out = _graph->_out; + + int i; + // Copy the data and apply the hanning window + for (i = 0; i < _windowSize; i++) { + _in[i] = window[ i ] * _hanning[ i ]; + } + + fftwf_execute(_graph->_plan); + + float b = _out[0] * _out[0]; + + _data_avg[0] += b; + if (b < _data_min[0]) _data_min[0] = b; + if (b > _data_max[0]) _data_max[0] = b; + + for (i=1; i < _dataSize - 1; i++) { // TODO: check with Jesse whether this is really correct + b = (_out[i] * _out[i]); + + _data_avg[i] += b; // + (_out[_windowSize-i] * _out[_windowSize-i]);, TODO: thanks to Stefan Kost + + if (_data_min[i] > b) _data_min[i] = b; + if (_data_max[i] < b ) _data_max[i] = b; + } + + _averages++; } @@ -59,21 +94,27 @@ FFTResult::finalize() _maximum = 0.0; return; } - + // Average & scale for (int i = 0; i < _dataSize; i++) { - _data[i] /= _averages; - _data[i] = 10.0f * log10f(_data[i]); + _data_avg[i] /= _averages; + _data_avg[i] = 10.0f * log10f(_data_avg[i]); + + _data_min[i] = 10.0f * log10f(_data_min[i]); + if (_data_min[i] < -10000.0f) { + _data_min[i] = -10000.0f; + } + _data_max[i] = 10.0f * log10f(_data_max[i]); } // find min & max - _minimum = _maximum = _data[0]; + _minimum = _maximum = _data_avg[0]; for (int i = 1; i < _dataSize; i++) { - if (_data[i] < _minimum && !isinf(_data[i])) { - _minimum = _data[i]; - } else if (_data[i] > _maximum && !isinf(_data[i])) { - _maximum = _data[i]; + if (_data_avg[i] < _minimum && !isinf(_data_avg[i])) { + _minimum = _data_avg[i]; + } else if (_data_avg[i] > _maximum && !isinf(_data_avg[i])) { + _maximum = _data_avg[i]; } } @@ -82,16 +123,36 @@ FFTResult::finalize() FFTResult::~FFTResult() { - free(_data); + free(_data_avg); + free(_data_min); + free(_data_max); } float -FFTResult::sampleAt(int x) +FFTResult::avgAt(int x) { if (x < 0 || x>= _dataSize) return 0.0f; - return _data[x]; + return _data_avg[x]; +} + +float +FFTResult::minAt(int x) +{ + if (x < 0 || x>= _dataSize) + return 0.0f; + + return _data_min[x]; +} + +float +FFTResult::maxAt(int x) +{ + if (x < 0 || x>= _dataSize) + return 0.0f; + + return _data_max[x]; } diff --git a/gtk2_ardour/fft_result.h b/gtk2_ardour/fft_result.h index c8f17dc01c..c6c952db1c 100644 --- a/gtk2_ardour/fft_result.h +++ b/gtk2_ardour/fft_result.h @@ -41,7 +41,9 @@ class FFTResult const int length() { return _dataSize; } - float sampleAt(int x); + float avgAt(int x); + float maxAt(int x); + float minAt(int x); const float minimum() { return _minimum; } const float maximum() { return _maximum; } @@ -53,10 +55,13 @@ class FFTResult int _averages; - float* _data; + float* _data_avg; + float* _data_max; + float* _data_min; + float* _work; - int _windowSize; + int _windowSize; int _dataSize; float _minimum;