1 /* Calf DSP Library 2 * Biquad filters 3 * Copyright (C) 2001-2007 Krzysztof Foltman 4 * 5 * Most of code in this file is based on freely 6 * available other work of other people (filter equations). 7 * 8 * This program is free software; you can redistribute it and/or 9 * modify it under the terms of the GNU Lesser General Public 10 * License as published by the Free Software Foundation; either 11 * version 2 of the License, or (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 * Lesser General Public License for more details. 17 * 18 * You should have received a copy of the GNU Lesser General 19 * Public License along with this program; if not, write to the 20 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 21 * Boston, MA 02110-1301 USA 22 */ 23 #ifndef __CALF_BIQUAD_H 24 #define __CALF_BIQUAD_H 25 26 #include <complex> 27 #include "primitives.h" 28 29 namespace dsp { 30 31 /** 32 * Coefficients for two-pole two-zero filter, for floating point values, 33 * plus a bunch of functions to set them to typical values. 34 * 35 * Coefficient calculation is based on famous Robert Bristow-Johnson's equations, 36 * except where it's not. 37 * The coefficient calculation is NOT mine, the only exception is the lossy 38 * optimization in Zoelzer and rbj HP filter code. 39 * 40 * See http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt for reference. 41 * 42 * don't use this for integers because it won't work 43 */ 44 template<class Coeff = float> 45 class biquad_coeffs 46 { 47 public: 48 // filter coefficients 49 Coeff a0, a1, a2, b1, b2; 50 typedef std::complex<double> cfloat; 51 biquad_coeffs()52 biquad_coeffs() 53 { 54 set_null(); 55 } 56 set_null()57 inline void set_null() 58 { 59 a0 = 1.0; 60 b1 = b2 = a1 = a2 = 0.f; 61 } 62 63 /** Lowpass filter based on Robert Bristow-Johnson's equations 64 * Perhaps every synth code that doesn't use SVF uses these 65 * equations :) 66 * @param fc resonant frequency 67 * @param q resonance (gain at fc) 68 * @param sr sample rate 69 * @param gain amplification (gain at 0Hz) 70 */ 71 inline void set_lp_rbj(float fc, float q, float sr, float gain = 1.0) 72 { 73 float omega=(float)(2*M_PI*fc/sr); 74 float sn=sin(omega); 75 float cs=cos(omega); 76 float alpha=(float)(sn/(2*q)); 77 float inv=(float)(1.0/(1.0+alpha)); 78 79 a2 = a0 = (float)(gain*inv*(1 - cs)*0.5f); 80 a1 = a0 + a0; 81 b1 = (float)(-2*cs*inv); 82 b2 = (float)((1 - alpha)*inv); 83 } 84 85 // different lowpass filter, based on Zoelzer's equations, modified by 86 // me (kfoltman) to use polynomials to approximate tangent function 87 // not very accurate, but perhaps good enough for synth work :) 88 // odsr is "one divided by samplerate" 89 // from how it looks, it perhaps uses bilinear transform - but who knows :) 90 inline void set_lp_zoelzer(float fc, float q, float odsr, float gain=1.0) 91 { 92 Coeff omega=(Coeff)(M_PI*fc*odsr); 93 Coeff omega2=omega*omega; 94 Coeff K=omega*(1+omega2*omega2*Coeff(1.0/1.45)); 95 Coeff KK=K*K; 96 Coeff QK=q*(KK+1.f); 97 Coeff iQK=1.0f/(QK+K); 98 Coeff inv=q*iQK; 99 b2 = (Coeff)(iQK*(QK-K)); 100 b1 = (Coeff)(2.f*(KK-1.f)*inv); 101 a2 = a0 = (Coeff)(inv*gain*KK); 102 a1 = a0 + a0; 103 } 104 105 /** Highpass filter based on Robert Bristow-Johnson's equations 106 * @param fc resonant frequency 107 * @param q resonance (gain at fc) 108 * @param sr sample rate 109 * @param gain amplification (gain at sr/2) 110 */ 111 inline void set_hp_rbj(float fc, float q, float esr, float gain=1.0) 112 { 113 Coeff omega=(float)(2*M_PI*fc/esr); 114 Coeff sn=sin(omega); 115 Coeff cs=cos(omega); 116 Coeff alpha=(float)(sn/(2*q)); 117 118 float inv=(float)(1.0/(1.0+alpha)); 119 120 a0 = (Coeff)(gain*inv*(1 + cs)/2); 121 a1 = -2.f * a0; 122 a2 = a0; 123 b1 = (Coeff)(-2*cs*inv); 124 b2 = (Coeff)((1 - alpha)*inv); 125 } 126 127 // this replaces sin/cos with polynomial approximation 128 inline void set_hp_rbj_optimized(float fc, float q, float esr, float gain=1.0) 129 { 130 Coeff omega=(float)(2*M_PI*fc/esr); 131 Coeff sn=omega+omega*omega*omega*(1.0/6.0)+omega*omega*omega*omega*omega*(1.0/120); 132 Coeff cs=1-omega*omega*(1.0/2.0)+omega*omega*omega*omega*(1.0/24); 133 Coeff alpha=(float)(sn/(2*q)); 134 135 float inv=(float)(1.0/(1.0+alpha)); 136 137 a0 = (Coeff)(gain*inv*(1 + cs)*(1.0/2.0)); 138 a1 = -2.f * a0; 139 a2 = a0; 140 b1 = (Coeff)(-2*cs*inv); 141 b2 = (Coeff)((1 - alpha)*inv); 142 } 143 144 /** Bandpass filter based on Robert Bristow-Johnson's equations (normalized to 1.0 at center frequency) 145 * @param fc center frequency (gain at fc = 1.0) 146 * @param q =~ fc/bandwidth (not quite, but close) - 1/Q = 2*sinh(ln(2)/2*BW*w0/sin(w0)) 147 * @param sr sample rate 148 * @param gain amplification (gain at sr/2) 149 */ 150 inline void set_bp_rbj(double fc, double q, double esr, double gain=1.0) 151 { 152 float omega=(float)(2*M_PI*fc/esr); 153 float sn=sin(omega); 154 float cs=cos(omega); 155 float alpha=(float)(sn/(2*q)); 156 157 float inv=(float)(1.0/(1.0+alpha)); 158 159 a0 = (float)(gain*inv*alpha); 160 a1 = 0.f; 161 a2 = (float)(-gain*inv*alpha); 162 b1 = (float)(-2*cs*inv); 163 b2 = (float)((1 - alpha)*inv); 164 } 165 166 // rbj's bandreject 167 inline void set_br_rbj(double fc, double q, double esr, double gain=1.0) 168 { 169 float omega=(float)(2*M_PI*fc/esr); 170 float sn=sin(omega); 171 float cs=cos(omega); 172 float alpha=(float)(sn/(2*q)); 173 174 float inv=(float)(1.0/(1.0+alpha)); 175 176 a0 = (Coeff)(gain*inv); 177 a1 = (Coeff)(-gain*inv*2*cs); 178 a2 = (Coeff)(gain*inv); 179 b1 = (Coeff)(-2*cs*inv); 180 b2 = (Coeff)((1 - alpha)*inv); 181 } 182 // this is mine (and, I guess, it sucks/doesn't work) set_allpass(float freq,float pole_r,float sr)183 void set_allpass(float freq, float pole_r, float sr) 184 { 185 float a=prewarp(freq, sr); 186 float q=pole_r; 187 set_bilinear(a*a+q*q, -2.0f*a, 1, a*a+q*q, 2.0f*a, 1); 188 } 189 /// prewarping for bilinear transform, maps given digital frequency to analog counterpart for analog filter design prewarp(float freq,float sr)190 static inline float prewarp(float freq, float sr) 191 { 192 if (freq>sr*0.49) freq=(float)(sr*0.49); 193 return (float)(tan(M_PI*freq/sr)); 194 } 195 /// convert analog angular frequency value to digital unwarp(float omega,float sr)196 static inline float unwarp(float omega, float sr) 197 { 198 float T = 1.0 / sr; 199 return (2 / T) * atan(omega * T / 2); 200 } 201 /// convert analog filter time constant to digital counterpart unwarpf(float t,float sr)202 static inline float unwarpf(float t, float sr) 203 { 204 // this is most likely broken and works by pure accident! 205 float omega = 1.0 / t; 206 omega = unwarp(omega, sr); 207 // I really don't know why does it have to be M_PI and not 2 * M_PI! 208 float f = M_PI / omega; 209 return f / sr; 210 } 211 /// set digital filter parameters based on given analog filter parameters set_bilinear(float aa0,float aa1,float aa2,float ab0,float ab1,float ab2)212 void set_bilinear(float aa0, float aa1, float aa2, float ab0, float ab1, float ab2) 213 { 214 float q=(float)(1.0/(ab0+ab1+ab2)); 215 a0 = (aa0+aa1+aa2)*q; 216 a1 = 2*(aa0-aa2)*q; 217 a2 = (aa0-aa1+aa2)*q; 218 b1 = 2*(ab0-ab2)*q; 219 b2 = (ab0-ab1+ab2)*q; 220 } 221 222 /// RBJ peaking EQ 223 /// @param freq peak frequency 224 /// @param q q (correlated to freq/bandwidth, @see set_bp_rbj) 225 /// @param peak peak gain (1.0 means no peak, >1.0 means a peak, less than 1.0 is a dip) set_peakeq_rbj(float freq,float q,float peak,float sr)226 inline void set_peakeq_rbj(float freq, float q, float peak, float sr) 227 { 228 float A = sqrt(peak); 229 float w0 = freq * 2 * M_PI * (1.0 / sr); 230 float alpha = sin(w0) / (2 * q); 231 float ib0 = 1.0 / (1 + alpha/A); 232 a1 = b1 = -2*cos(w0) * ib0; 233 a0 = ib0 * (1 + alpha*A); 234 a2 = ib0 * (1 - alpha*A); 235 b2 = ib0 * (1 - alpha/A); 236 } 237 238 /// RBJ low shelf EQ - amplitication of 'peak' at 0 Hz and of 1.0 (0dB) at sr/2 Hz 239 /// @param freq corner frequency (gain at freq is sqrt(peak)) 240 /// @param q q (relates bandwidth and peak frequency), the higher q, the louder the resonant peak (situated below fc) is 241 /// @param peak shelf gain (1.0 means no peak, >1.0 means a peak, less than 1.0 is a dip) set_lowshelf_rbj(float freq,float q,float peak,float sr)242 inline void set_lowshelf_rbj(float freq, float q, float peak, float sr) 243 { 244 float A = sqrt(peak); 245 float w0 = freq * 2 * M_PI * (1.0 / sr); 246 float alpha = sin(w0) / (2 * q); 247 float cw0 = cos(w0); 248 float tmp = 2 * sqrt(A) * alpha; 249 float b0 = 0.f, ib0 = 0.f; 250 251 a0 = A*( (A+1) - (A-1)*cw0 + tmp); 252 a1 = 2*A*( (A-1) - (A+1)*cw0); 253 a2 = A*( (A+1) - (A-1)*cw0 - tmp); 254 b0 = (A+1) + (A-1)*cw0 + tmp; 255 b1 = -2*( (A-1) + (A+1)*cw0); 256 b2 = (A+1) + (A-1)*cw0 - tmp; 257 258 ib0 = 1.0 / b0; 259 b1 *= ib0; 260 b2 *= ib0; 261 a0 *= ib0; 262 a1 *= ib0; 263 a2 *= ib0; 264 } 265 266 /// RBJ high shelf EQ - amplitication of 0dB at 0 Hz and of peak at sr/2 Hz 267 /// @param freq corner frequency (gain at freq is sqrt(peak)) 268 /// @param q q (relates bandwidth and peak frequency), the higher q, the louder the resonant peak (situated above fc) is 269 /// @param peak shelf gain (1.0 means no peak, >1.0 means a peak, less than 1.0 is a dip) set_highshelf_rbj(float freq,float q,float peak,float sr)270 inline void set_highshelf_rbj(float freq, float q, float peak, float sr) 271 { 272 float A = sqrt(peak); 273 float w0 = freq * 2 * M_PI * (1.0 / sr); 274 float alpha = sin(w0) / (2 * q); 275 float cw0 = cos(w0); 276 float tmp = 2 * sqrt(A) * alpha; 277 float b0 = 0.f, ib0 = 0.f; 278 279 a0 = A*( (A+1) + (A-1)*cw0 + tmp); 280 a1 = -2*A*( (A-1) + (A+1)*cw0); 281 a2 = A*( (A+1) + (A-1)*cw0 - tmp); 282 b0 = (A+1) - (A-1)*cw0 + tmp; 283 b1 = 2*( (A-1) - (A+1)*cw0); 284 b2 = (A+1) - (A-1)*cw0 - tmp; 285 286 ib0 = 1.0 / b0; 287 b1 *= ib0; 288 b2 *= ib0; 289 a0 *= ib0; 290 a1 *= ib0; 291 a2 *= ib0; 292 } 293 294 /// copy coefficients from another biquad 295 template<class U> copy_coeffs(const biquad_coeffs<U> & src)296 inline void copy_coeffs(const biquad_coeffs<U> &src) 297 { 298 a0 = src.a0; 299 a1 = src.a1; 300 a2 = src.a2; 301 b1 = src.b1; 302 b2 = src.b2; 303 } 304 305 /// Return the filter's gain at frequency freq 306 /// @param freq Frequency to look up 307 /// @param sr Filter sample rate (used to convert frequency to angular frequency) freq_gain(float freq,float sr)308 float freq_gain(float freq, float sr) const 309 { 310 typedef std::complex<double> cfloat; 311 freq *= 2.0 * M_PI / sr; 312 cfloat z = 1.0 / exp(cfloat(0.0, freq)); 313 314 return std::abs(h_z(z)); 315 } 316 317 /// Return H(z) the filter's gain at frequency freq 318 /// @param z Z variable (e^jw) h_z(const cfloat & z)319 cfloat h_z(const cfloat &z) const 320 { 321 322 return (cfloat(a0) + double(a1) * z + double(a2) * z*z) / (cfloat(1.0) + double(b1) * z + double(b2) * z*z); 323 } 324 325 }; 326 327 /** 328 * Two-pole two-zero filter, for floating point values. 329 * Uses "traditional" Direct I form (separate FIR and IIR halves). 330 * don't use this for integers because it won't work 331 */ 332 template<class Coeff = float, class T = float> 333 struct biquad_d1: public biquad_coeffs<Coeff> 334 { 335 using biquad_coeffs<Coeff>::a0; 336 using biquad_coeffs<Coeff>::a1; 337 using biquad_coeffs<Coeff>::a2; 338 using biquad_coeffs<Coeff>::b1; 339 using biquad_coeffs<Coeff>::b2; 340 /// input[n-1] 341 T x1; 342 /// input[n-2] 343 T x2; 344 /// output[n-1] 345 T y1; 346 /// output[n-2] 347 T y2; 348 /// Constructor (initializes state to all zeros) biquad_d1biquad_d1349 biquad_d1() 350 { 351 reset(); 352 } 353 /// direct I form with four state variables processbiquad_d1354 inline T process(T in) 355 { 356 T out = in * a0 + x1 * a1 + x2 * a2 - y1 * b1 - y2 * b2; 357 x2 = x1; 358 y2 = y1; 359 x1 = in; 360 y1 = out; 361 return out; 362 } 363 364 /// direct I form with zero input process_zeroinbiquad_d1365 inline T process_zeroin() 366 { 367 T out = - y1 * b1 - y2 * b2; 368 y2 = y1; 369 y1 = out; 370 return out; 371 } 372 373 /// simplified version for lowpass case with two zeros at -1 process_lpbiquad_d1374 inline T process_lp(T in) 375 { 376 T out = a0*(in + x1 + x1 + x2) - y1 * b1 - y2 * b2; 377 x2 = x1; 378 y2 = y1; 379 x1 = in; 380 y1 = out; 381 return out; 382 } 383 /// Sanitize (set to 0 if potentially denormal) filter state sanitizebiquad_d1384 inline void sanitize() 385 { 386 dsp::sanitize(x1); 387 dsp::sanitize(y1); 388 dsp::sanitize(x2); 389 dsp::sanitize(y2); 390 } 391 /// Reset state variables resetbiquad_d1392 inline void reset() 393 { 394 dsp::zero(x1); 395 dsp::zero(y1); 396 dsp::zero(x2); 397 dsp::zero(y2); 398 } emptybiquad_d1399 inline bool empty() const { 400 return (y1 == 0.f && y2 == 0.f); 401 } 402 403 }; 404 405 /** 406 * Two-pole two-zero filter, for floating point values. 407 * Uses slightly faster Direct II form (combined FIR and IIR halves). 408 * However, when used with wildly varying coefficients, it may 409 * make more zipper noise than Direct I form, so it's better to 410 * use it when filter coefficients are not changed mid-stream. 411 */ 412 template<class Coeff = float, class T = float> 413 struct biquad_d2: public biquad_coeffs<Coeff> 414 { 415 using biquad_coeffs<Coeff>::a0; 416 using biquad_coeffs<Coeff>::a1; 417 using biquad_coeffs<Coeff>::a2; 418 using biquad_coeffs<Coeff>::b1; 419 using biquad_coeffs<Coeff>::b2; 420 /// state[n-1] 421 float w1; 422 /// state[n-2] 423 float w2; 424 /// Constructor (initializes state to all zeros) biquad_d2biquad_d2425 biquad_d2() 426 { 427 reset(); 428 } 429 /// direct II form with two state variables processbiquad_d2430 inline T process(T in) 431 { 432 dsp::sanitize_denormal(in); 433 dsp::sanitize(in); 434 dsp::sanitize(w1); 435 dsp::sanitize(w2); 436 437 T tmp = in - w1 * b1 - w2 * b2; 438 T out = tmp * a0 + w1 * a1 + w2 * a2; 439 w2 = w1; 440 w1 = tmp; 441 return out; 442 } 443 444 // direct II form with two state variables, lowpass version 445 // interesting fact: this is actually slower than the general version! process_lpbiquad_d2446 inline T process_lp(T in) 447 { 448 T tmp = in - w1 * b1 - w2 * b2; 449 T out = (tmp + w2 + w1* 2) * a0; 450 w2 = w1; 451 w1 = tmp; 452 return out; 453 } 454 455 /// Is the filter state completely silent? (i.e. set to 0 by sanitize function) emptybiquad_d2456 inline bool empty() const { 457 return (w1 == 0.f && w2 == 0.f); 458 } 459 460 461 /// Sanitize (set to 0 if potentially denormal) filter state sanitizebiquad_d2462 inline void sanitize() 463 { 464 dsp::sanitize(w1); 465 dsp::sanitize(w2); 466 } 467 468 /// Reset state variables resetbiquad_d2469 inline void reset() 470 { 471 dsp::zero(w1); 472 dsp::zero(w2); 473 } 474 }; 475 476 /** 477 * Two-pole two-zero filter, for floating point values. 478 * Uses "traditional" Direct I form (separate FIR and IIR halves). 479 * don't use this for integers because it won't work 480 */ 481 template<class Coeff = float, class T = float> 482 struct biquad_d1_lerp: public biquad_coeffs<Coeff> 483 { 484 using biquad_coeffs<Coeff>::a0; 485 using biquad_coeffs<Coeff>::a1; 486 using biquad_coeffs<Coeff>::a2; 487 using biquad_coeffs<Coeff>::b1; 488 using biquad_coeffs<Coeff>::b2; 489 Coeff a0cur, a1cur, a2cur, b1cur, b2cur; 490 Coeff a0delta, a1delta, a2delta, b1delta, b2delta; 491 /// input[n-1] 492 T x1; 493 /// input[n-2] 494 T x2; 495 /// output[n-1] 496 T y1; 497 /// output[n-2] 498 T y2; 499 /// Constructor (initializes state to all zeros) biquad_d1_lerpbiquad_d1_lerp500 biquad_d1_lerp() 501 { 502 reset(); 503 } 504 #define _DO_COEFF(coeff) coeff##delta = (coeff - coeff##cur) * (frac) big_stepbiquad_d1_lerp505 void big_step(Coeff frac) 506 { 507 _DO_COEFF(a0); 508 _DO_COEFF(a1); 509 _DO_COEFF(a2); 510 _DO_COEFF(b1); 511 _DO_COEFF(b2); 512 } 513 #undef _DO_COEFF 514 /// direct I form with four state variables processbiquad_d1_lerp515 inline T process(T in) 516 { 517 T out = in * a0cur + x1 * a1cur + x2 * a2cur - y1 * b1cur - y2 * b2cur; 518 x2 = x1; 519 y2 = y1; 520 x1 = in; 521 y1 = out; 522 a0cur += a0delta; 523 a1cur += a1delta; 524 a2cur += a2delta; 525 b1cur += b1delta; 526 b2cur += b2delta; 527 return out; 528 } 529 530 /// direct I form with zero input process_zeroinbiquad_d1_lerp531 inline T process_zeroin() 532 { 533 T out = - y1 * b1 - y2 * b2; 534 y2 = y1; 535 y1 = out; 536 b1cur += b1delta; 537 b2cur += b2delta; 538 return out; 539 } 540 541 /// simplified version for lowpass case with two zeros at -1 process_lpbiquad_d1_lerp542 inline T process_lp(T in) 543 { 544 T out = a0*(in + x1 + x1 + x2) - y1 * b1 - y2 * b2; 545 x2 = x1; 546 y2 = y1; 547 x1 = in; 548 y1 = out; 549 return out; 550 } 551 /// Sanitize (set to 0 if potentially denormal) filter state sanitizebiquad_d1_lerp552 inline void sanitize() 553 { 554 dsp::sanitize(x1); 555 dsp::sanitize(y1); 556 dsp::sanitize(x2); 557 dsp::sanitize(y2); 558 dsp::sanitize(a0cur); 559 dsp::sanitize(a1cur); 560 dsp::sanitize(a2cur); 561 dsp::sanitize(b1cur); 562 dsp::sanitize(b2cur); 563 } 564 /// Reset state variables resetbiquad_d1_lerp565 inline void reset() 566 { 567 dsp::zero(x1); 568 dsp::zero(y1); 569 dsp::zero(x2); 570 dsp::zero(y2); 571 dsp::zero(a0cur); 572 dsp::zero(a1cur); 573 dsp::zero(a2cur); 574 dsp::zero(b1cur); 575 dsp::zero(b2cur); 576 } emptybiquad_d1_lerp577 inline bool empty() { 578 return (y1 == 0.f && y2 == 0.f); 579 } 580 581 }; 582 583 /// Compose two filters in series 584 template<class F1, class F2> 585 class filter_compose { 586 public: 587 typedef std::complex<float> cfloat; 588 F1 f1; 589 F2 f2; 590 public: process(float value)591 float process(float value) { 592 return f2.process(f1.process(value)); 593 } 594 h_z(const cfloat & z)595 inline cfloat h_z(const cfloat &z) const { 596 return f1.h_z(z) * f2.h_z(z); 597 } 598 599 /// Return the filter's gain at frequency freq 600 /// @param freq Frequency to look up 601 /// @param sr Filter sample rate (used to convert frequency to angular frequency) freq_gain(float freq,float sr)602 float freq_gain(float freq, float sr) const 603 { 604 typedef std::complex<double> cfloat; 605 freq *= 2.0 * M_PI / sr; 606 cfloat z = 1.0 / exp(cfloat(0.0, freq)); 607 608 return std::abs(h_z(z)); 609 } 610 sanitize()611 void sanitize() { 612 f1.sanitize(); 613 f2.sanitize(); 614 } 615 }; 616 617 /// Compose two filters in parallel 618 template<class F1, class F2> 619 class filter_sum { 620 public: 621 typedef std::complex<double> cfloat; 622 F1 f1; 623 F2 f2; 624 public: process(float value)625 float process(float value) { 626 return f2.process(value) + f1.process(value); 627 } 628 h_z(const cfloat & z)629 inline cfloat h_z(const cfloat &z) const { 630 return f1.h_z(z) + f2.h_z(z); 631 } 632 633 /// Return the filter's gain at frequency freq 634 /// @param freq Frequency to look up 635 /// @param sr Filter sample rate (used to convert frequency to angular frequency) freq_gain(float freq,float sr)636 float freq_gain(float freq, float sr) const 637 { 638 typedef std::complex<double> cfloat; 639 freq *= 2.0 * M_PI / sr; 640 cfloat z = 1.0 / exp(cfloat(0.0, freq)); 641 642 return std::abs(h_z(z)); 643 } 644 sanitize()645 void sanitize() { 646 f1.sanitize(); 647 f2.sanitize(); 648 } 649 }; 650 651 }; 652 653 #endif 654