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 }