spek

Acoustic spectrum analyser https://github.com/alexkay/spek spek.cc
git clone http://git.hanabi.in/repos/spek.git
Log | Files | Refs | README

spek-spectrogram.cc (13096B)


      1 #include <cmath>
      2 
      3 #include <wx/dcbuffer.h>
      4 
      5 #include "spek-audio.h"
      6 #include "spek-events.h"
      7 #include "spek-fft.h"
      8 #include "spek-platform.h"
      9 #include "spek-ruler.h"
     10 #include "spek-utils.h"
     11 
     12 #include "spek-spectrogram.h"
     13 
     14 BEGIN_EVENT_TABLE(SpekSpectrogram, wxWindow)
     15     EVT_CHAR(SpekSpectrogram::on_char)
     16     EVT_PAINT(SpekSpectrogram::on_paint)
     17     EVT_SIZE(SpekSpectrogram::on_size)
     18     SPEK_EVT_HAVE_SAMPLE(SpekSpectrogram::on_have_sample)
     19 END_EVENT_TABLE()
     20 
     21 enum
     22 {
     23     MIN_RANGE = -140,
     24     MAX_RANGE = 0,
     25     URANGE = 0,
     26     LRANGE = -120,
     27     FFT_BITS = 11,
     28     MIN_FFT_BITS = 8,
     29     MAX_FFT_BITS = 14,
     30     LPAD = 60,
     31     TPAD = 60,
     32     RPAD = 90,
     33     BPAD = 40,
     34     GAP = 10,
     35     RULER = 10,
     36 };
     37 
     38 // Forward declarations.
     39 static wxString trim(wxDC& dc, const wxString& s, int length, bool trim_end);
     40 static int bits_to_bands(int bits);
     41 
     42 SpekSpectrogram::SpekSpectrogram(wxFrame *parent) :
     43     wxWindow(
     44         parent, -1, wxDefaultPosition, wxDefaultSize,
     45         wxFULL_REPAINT_ON_RESIZE | wxWANTS_CHARS
     46     ),
     47     audio(new Audio()), // TODO: refactor
     48     fft(new FFT()),
     49     pipeline(NULL),
     50     streams(0),
     51     stream(0),
     52     channels(0),
     53     channel(0),
     54     window_function(WINDOW_DEFAULT),
     55     duration(0.0),
     56     sample_rate(0),
     57     palette(PALETTE_DEFAULT),
     58     palette_image(),
     59     image(1, 1),
     60     prev_width(-1),
     61     fft_bits(FFT_BITS),
     62     urange(URANGE),
     63     lrange(LRANGE)
     64 {
     65     this->create_palette();
     66 
     67     SetBackgroundStyle(wxBG_STYLE_CUSTOM);
     68     SetFocus();
     69 }
     70 
     71 SpekSpectrogram::~SpekSpectrogram()
     72 {
     73     this->stop();
     74 }
     75 
     76 void SpekSpectrogram::open(const wxString& path)
     77 {
     78     this->path = path;
     79     this->stream = 0;
     80     this->channel = 0;
     81     start();
     82     Refresh();
     83 }
     84 
     85 void SpekSpectrogram::save(const wxString& path)
     86 {
     87     wxSize size = GetClientSize();
     88     wxBitmap bitmap(size.GetWidth(), size.GetHeight());
     89     wxMemoryDC dc(bitmap);
     90     render(dc);
     91     bitmap.SaveFile(path, wxBITMAP_TYPE_PNG);
     92 }
     93 
     94 void SpekSpectrogram::on_char(wxKeyEvent& evt)
     95 {
     96     switch (evt.GetKeyCode()) {
     97     case 'c':
     98         if (this->channels) {
     99             this->channel = (this->channel + 1) % this->channels;
    100         }
    101         break;
    102     case 'C':
    103         if (this->channels) {
    104             this->channel = (this->channel - 1 + this->channels) % this->channels;
    105         }
    106         break;
    107     case 'f':
    108         this->window_function = (enum window_function) ((this->window_function + 1) % WINDOW_COUNT);
    109         break;
    110     case 'F':
    111         this->window_function =
    112             (enum window_function) ((this->window_function - 1 + WINDOW_COUNT) % WINDOW_COUNT);
    113         break;
    114     case 'l':
    115         this->lrange = spek_min(this->lrange + 1, this->urange - 1);
    116         break;
    117     case 'L':
    118         this->lrange = spek_max(this->lrange - 1, MIN_RANGE);
    119         break;
    120     case 'p':
    121         this->palette = (enum palette) ((this->palette + 1) % PALETTE_COUNT);
    122         this->create_palette();
    123         break;
    124     case 'P':
    125         this->palette = (enum palette) ((this->palette - 1 + PALETTE_COUNT) % PALETTE_COUNT);
    126         this->create_palette();
    127         break;
    128     case 's':
    129         if (this->streams) {
    130             this->stream = (this->stream + 1) % this->streams;
    131         }
    132         break;
    133     case 'S':
    134         if (this->streams) {
    135             this->stream = (this->stream - 1 + this->streams) % this->streams;
    136         }
    137         break;
    138     case 'u':
    139         this->urange = spek_min(this->urange + 1, MAX_RANGE);
    140         break;
    141     case 'U':
    142         this->urange = spek_max(this->urange - 1, this->lrange + 1);
    143         break;
    144     case 'w':
    145         this->fft_bits = spek_min(this->fft_bits + 1, MAX_FFT_BITS);
    146         this->create_palette();
    147         break;
    148     case 'W':
    149         this->fft_bits = spek_max(this->fft_bits - 1, MIN_FFT_BITS);
    150         this->create_palette();
    151         break;
    152     default:
    153         evt.Skip();
    154         return;
    155     }
    156 
    157     start();
    158     Refresh();
    159 }
    160 
    161 void SpekSpectrogram::on_paint(wxPaintEvent&)
    162 {
    163     wxAutoBufferedPaintDC dc(this);
    164     render(dc);
    165 }
    166 
    167 void SpekSpectrogram::on_size(wxSizeEvent&)
    168 {
    169     wxSize size = GetClientSize();
    170     bool width_changed = this->prev_width != size.GetWidth();
    171     this->prev_width = size.GetWidth();
    172 
    173     if (width_changed) {
    174         start();
    175     }
    176 }
    177 
    178 void SpekSpectrogram::on_have_sample(SpekHaveSampleEvent& event)
    179 {
    180     int bands = event.get_bands();
    181     int sample = event.get_sample();
    182     const float *values = event.get_values();
    183 
    184     if (sample == -1) {
    185         this->stop();
    186         return;
    187     }
    188 
    189     // TODO: check image size, quit if wrong.
    190     double range = this->urange - this->lrange;
    191     for (int y = 0; y < bands; y++) {
    192         double value = fmin(this->urange, fmax(this->lrange, values[y]));
    193         double level = (value - this->lrange) / range;
    194         uint32_t color = spek_palette(this->palette, level);
    195         this->image.SetRGB(
    196             sample,
    197             bands - y - 1,
    198             color >> 16,
    199             (color >> 8) & 0xFF,
    200             color & 0xFF
    201         );
    202     }
    203 
    204     // TODO: refresh only one pixel column
    205     this->Refresh();
    206 }
    207 
    208 static wxString time_formatter(int unit)
    209 {
    210     // TODO: i18n
    211     return wxString::Format("%d:%02d", unit / 60, unit % 60);
    212 }
    213 
    214 static wxString freq_formatter(int unit)
    215 {
    216     return wxString::Format(_("%d kHz"), unit / 1000);
    217 }
    218 
    219 static wxString density_formatter(int unit)
    220 {
    221     return wxString::Format(_("%d dB"), -unit);
    222 }
    223 
    224 void SpekSpectrogram::render(wxDC& dc)
    225 {
    226     wxSize size = GetClientSize();
    227     int w = size.GetWidth();
    228     int h = size.GetHeight();
    229 
    230     // Initialise.
    231     dc.SetBackground(*wxBLACK_BRUSH);
    232     dc.SetBackgroundMode(wxTRANSPARENT);
    233     dc.SetPen(*wxWHITE_PEN);
    234     dc.SetBrush(*wxTRANSPARENT_BRUSH);
    235     dc.SetTextForeground(wxColour(255, 255, 255));
    236     wxFont normal_font = wxFont(
    237         (int)round(9 * spek_platform_font_scale()),
    238         wxFONTFAMILY_SWISS,
    239         wxFONTSTYLE_NORMAL,
    240         wxFONTWEIGHT_NORMAL
    241     );
    242     wxFont large_font = wxFont(normal_font);
    243     large_font.SetPointSize((int)round(10 * spek_platform_font_scale()));
    244     large_font.SetWeight(wxFONTWEIGHT_BOLD);
    245     wxFont small_font = wxFont(normal_font);
    246     small_font.SetPointSize((int)round(8 * spek_platform_font_scale()));
    247     dc.SetFont(normal_font);
    248     int normal_height = dc.GetTextExtent("dummy").GetHeight();
    249     dc.SetFont(large_font);
    250     int large_height = dc.GetTextExtent("dummy").GetHeight();
    251     dc.SetFont(small_font);
    252     int small_height = dc.GetTextExtent("dummy").GetHeight();
    253 
    254     // Clean the background.
    255     dc.Clear();
    256 
    257     // Spek version
    258     dc.SetFont(large_font);
    259     wxString package_name(PACKAGE_NAME);
    260     dc.DrawText(
    261         package_name,
    262         w - RPAD + GAP,
    263         TPAD - 2 * GAP - normal_height - large_height
    264     );
    265     int package_name_width = dc.GetTextExtent(package_name + " ").GetWidth();
    266     dc.SetFont(small_font);
    267     dc.DrawText(
    268         PACKAGE_VERSION,
    269         w - RPAD + GAP + package_name_width,
    270         TPAD - 2 * GAP - normal_height - small_height
    271     );
    272 
    273     if (this->image.GetWidth() > 1 && this->image.GetHeight() > 1 &&
    274         w - LPAD - RPAD > 0 && h - TPAD - BPAD > 0) {
    275         // Draw the spectrogram.
    276         wxBitmap bmp(this->image.Scale(w - LPAD - RPAD, h - TPAD - BPAD));
    277         dc.DrawBitmap(bmp, LPAD, TPAD);
    278 
    279         // File name.
    280         dc.SetFont(large_font);
    281         dc.DrawText(
    282             trim(dc, this->path, w - LPAD - RPAD, false),
    283             LPAD,
    284             TPAD - 2 * GAP - normal_height - large_height
    285         );
    286 
    287         // File properties.
    288         dc.SetFont(normal_font);
    289         dc.DrawText(
    290             trim(dc, this->desc, w - LPAD - RPAD, true),
    291             LPAD,
    292             TPAD - GAP - normal_height
    293         );
    294 
    295         // Prepare to draw the rulers.
    296         dc.SetFont(small_font);
    297 
    298         if (this->duration) {
    299             // Time ruler.
    300             int time_factors[] = {1, 2, 5, 10, 20, 30, 1*60, 2*60, 5*60, 10*60, 20*60, 30*60, 0};
    301             SpekRuler time_ruler(
    302                 LPAD,
    303                 h - BPAD,
    304                 SpekRuler::BOTTOM,
    305                 // TODO: i18n
    306                 "00:00",
    307                 time_factors,
    308                 0,
    309                 (int)this->duration,
    310                 1.5,
    311                 (w - LPAD - RPAD) / this->duration,
    312                 0.0,
    313                 time_formatter
    314                 );
    315             time_ruler.draw(dc);
    316         }
    317 
    318         if (this->sample_rate) {
    319             // Frequency ruler.
    320             int freq = this->sample_rate / 2;
    321             int freq_factors[] = {1000, 2000, 5000, 10000, 20000, 0};
    322             SpekRuler freq_ruler(
    323                 LPAD,
    324                 TPAD,
    325                 SpekRuler::LEFT,
    326                 // TRANSLATORS: keep "00" unchanged, it's used to calc the text width
    327                 _("00 kHz"),
    328                 freq_factors,
    329                 0,
    330                 freq,
    331                 3.0,
    332                 (h - TPAD - BPAD) / (double)freq,
    333                 0.0,
    334                 freq_formatter
    335                 );
    336             freq_ruler.draw(dc);
    337         }
    338     }
    339 
    340     // Border around the spectrogram.
    341     dc.DrawRectangle(LPAD, TPAD, w - LPAD - RPAD, h - TPAD - BPAD);
    342 
    343     // The palette.
    344     if (h - TPAD - BPAD > 0) {
    345         wxBitmap bmp(this->palette_image.Scale(RULER, h - TPAD - BPAD + 1));
    346         dc.DrawBitmap(bmp, w - RPAD + GAP, TPAD);
    347 
    348         // Prepare to draw the ruler.
    349         dc.SetFont(small_font);
    350 
    351         // Spectral density.
    352         int density_factors[] = {1, 2, 5, 10, 20, 50, 0};
    353         SpekRuler density_ruler(
    354             w - RPAD + GAP + RULER,
    355             TPAD,
    356             SpekRuler::RIGHT,
    357             // TRANSLATORS: keep "-00" unchanged, it's used to calc the text width
    358             _("-00 dB"),
    359             density_factors,
    360             -this->urange,
    361             -this->lrange,
    362             3.0,
    363             (h - TPAD - BPAD) / (double)(this->lrange - this->urange),
    364             h - TPAD - BPAD,
    365             density_formatter
    366         );
    367         density_ruler.draw(dc);
    368     }
    369 }
    370 
    371 static void pipeline_cb(int bands, int sample, float *values, void *cb_data)
    372 {
    373     SpekHaveSampleEvent event(bands, sample, values, false);
    374     SpekSpectrogram *s = (SpekSpectrogram *)cb_data;
    375     wxPostEvent(s, event);
    376 }
    377 
    378 void SpekSpectrogram::start()
    379 {
    380     if (this->path.IsEmpty()) {
    381         return;
    382     }
    383 
    384     this->stop();
    385 
    386     // The number of samples is the number of pixels available for the image.
    387     // The number of bands is fixed, FFT results are very different for
    388     // different values but we need some consistency.
    389     wxSize size = GetClientSize();
    390     int samples = size.GetWidth() - LPAD - RPAD;
    391     if (samples > 0) {
    392         this->image.Create(samples, bits_to_bands(this->fft_bits));
    393         this->pipeline = spek_pipeline_open(
    394             this->audio->open(std::string(this->path.utf8_str()), this->stream),
    395             this->fft->create(this->fft_bits),
    396             this->stream,
    397             this->channel,
    398             this->window_function,
    399             samples,
    400             pipeline_cb,
    401             this
    402         );
    403         spek_pipeline_start(this->pipeline);
    404         // TODO: extract conversion into a utility function.
    405         this->desc = wxString::FromUTF8(spek_pipeline_desc(this->pipeline).c_str());
    406         this->streams = spek_pipeline_streams(this->pipeline);
    407         this->channels = spek_pipeline_channels(this->pipeline);
    408         this->duration = spek_pipeline_duration(this->pipeline);
    409         this->sample_rate = spek_pipeline_sample_rate(this->pipeline);
    410     } else {
    411         this->image.Create(1, 1);
    412     }
    413 }
    414 
    415 void SpekSpectrogram::stop()
    416 {
    417     if (this->pipeline) {
    418         spek_pipeline_close(this->pipeline);
    419         this->pipeline = NULL;
    420 
    421         // Make sure all have_sample events are processed before returning.
    422         wxApp::GetInstance()->ProcessPendingEvents();
    423     }
    424 }
    425 
    426 void SpekSpectrogram::create_palette()
    427 {
    428     this->palette_image.Create(RULER, bits_to_bands(this->fft_bits));
    429     for (int y = 0; y < bits_to_bands(this->fft_bits); y++) {
    430         uint32_t color = spek_palette(this->palette, y / (double)bits_to_bands(this->fft_bits));
    431         this->palette_image.SetRGB(
    432             wxRect(0, bits_to_bands(this->fft_bits) - y - 1, RULER, 1),
    433             color >> 16,
    434             (color >> 8) & 0xFF,
    435             color & 0xFF
    436         );
    437     }
    438 }
    439 
    440 // Trim `s` so that it fits into `length`.
    441 static wxString trim(wxDC& dc, const wxString& s, int length, bool trim_end)
    442 {
    443     if (length <= 0) {
    444         return wxEmptyString;
    445     }
    446 
    447     // Check if the entire string fits.
    448     wxSize size = dc.GetTextExtent(s);
    449     if (size.GetWidth() <= length) {
    450         return s;
    451     }
    452 
    453     // Binary search FTW!
    454     wxString fix("...");
    455     int i = 0;
    456     int k = s.length();
    457     while (k - i > 1) {
    458         int j = (i + k) / 2;
    459         size = dc.GetTextExtent(trim_end ? s.substr(0, j) + fix : fix + s.substr(j));
    460         if (trim_end != (size.GetWidth() > length)) {
    461             i = j;
    462         } else {
    463             k = j;
    464         }
    465     }
    466 
    467     return trim_end ? s.substr(0, i) + fix : fix + s.substr(k);
    468 }
    469 
    470 // TODO: test
    471 static int bits_to_bands(int bits) {
    472     return (1 << (bits - 1)) + 1;
    473 }