11

I am rendering a waveform in PHP by downsampling it with the lame encoder and then drawing the waveform from the resulting data points. I am currently getting images like this:

enter image description here

What I would like to do is modify my code so that the apparent dynamic range of the waveform is essentially 'compressed'. To produce a waveform that looks more like this:

enter image description here

The equation I am currently using to render the height of each data point is as follows:-

 // draw this data point
          // relative value based on height of image being generated
          // data values can range between 0 and 255
           $v = (int) ( $data / 255 * $height );


          // don't print flat values on the canvas if not necessary
          if (!($v / $height == 0.5 && !$draw_flat))
            // draw the line on the image using the $v value and centering it vertically on the canvas
            imageline(
              $img,
              // x1
              (int) ($data_point / DETAIL),
              // y1: height of the image minus $v as a percentage of the height for the wave amplitude
              $height * $wav - $v,
              // x2
              (int) ($data_point / DETAIL),
              // y2: same as y1, but from the bottom of the image
              $height * $wav - ($height - $v),
              imagecolorallocate($img, $r, $g, $b)
            );      

With the actual amplitude being defined by the first line of this code:-

  $v = (int) ( $data / 255 * $height );

Unfortunately my math skill is poor at best. What I need to do is essentially apply a 'curve' to the value of $v so that when the number input into the equation is lower, the resulting output is higher and as the input number is increased the equation reduces the amplification until finally when the input reaches 255 the output should also be 255. Also the curve should be such so that with an input of 0 the output is also 0.

I apologise if this is not clear but I am finding this question very hard to articulate with my limited math experience.

Perhaps a visual representation would help describe my intent:-

enter image description here

When the value of $v is either 0 or 255 the output of the equation should be exactly the input (0 or 255). However, when the input is a value inbetween, it should follow the resulting output of the curve above. (the above was only a rough drawing to illustrate.)

EDIT:

Based on Alnitiks 'pow' function solution I am now generating waveforms that look like this:-

enter image description here

Using the replacement equation for the $v variable as follows:-

 $v = pow($data / 255.0, 0.4) * $height;

I have tried upping the 0.4 value but the result is still not as intended.

EDIT 2:

As requested here is a raw datadump of my $data variable:

Raw Data

This gets passed into the equation to return $v before being used to draw the waveform (you can see what I do to variable $v in the original code I posted above. $height is simple the number of pixels high I have set the image to render.

This data is a comma seperated list of values. I hope this helps. It appears your assertion that the mean value is 128 is correct. So far I have been unable to get my head around your correction for this. I'm afraid it is slightly beyond my current understanding.

gordyr
  • 5,618
  • 13
  • 51
  • 118
  • 1
    +1: I'm not sure whether this is strictly on-topic, but I don't care... it's fun! – Lightness Races in Orbit Jan 03 '12 at 15:12
  • Really you need to find the equation of that curve and apply a multiplication factor; that's all this boils down to when you strip the fun factor out. And you could skip that entirely and just manually create a lookup of known values -- as hakre suggests -- if you don't care about fractional values (which seems likely). – Lightness Races in Orbit Jan 03 '12 at 15:13
  • 1
    Indeed. I did wonder whether this should have been posted on the math site. But i'm certain the responses I would get from you guys would be far more in my 'own' language than a mathematicians. :-) – gordyr Jan 03 '12 at 15:16
  • +1 And it's a well written question, too. – hakre Jan 03 '12 at 15:19
  • 1
    the required curve is a gamma correction curve - `y = pow(x, 1 / gamma)` – Alnitak Jan 03 '12 at 15:25
  • @hakre: Yea, that certainly tips the balance too :) – Lightness Races in Orbit Jan 03 '12 at 15:26
  • the point of the correction to my answer is to make the power-based scaling _symmetrical around the mean value_ It treats the values as 128 +/- 127, instead of 0 - 255. – Alnitak Jan 03 '12 at 20:32
  • see graph at http://bit.ly/wZUqxs to see what I mean – Alnitak Jan 03 '12 at 20:45
  • While I understand the concept, I think the problem is more to do with my PHP implementation of your equation and its integration into my current code/method. I'll keep playing with it. You've truly been a huge help and I really cannot thank you enough. – gordyr Jan 03 '12 at 21:13

3 Answers3

3

With no math skills (and probably useful to have a speedy display):

You have 256 possible values. Create an array that contains the "dynamic" value for each of these values:

$dynamic = array(
   0 => 0,
   1 => 2,
   ...
);

That done, you can easily get the dynamic value:

$v = (int) ($dynamic[(int) $data / 255] * $height);

You might lose some precision, but it's probably useful.


Natural dynamic values are generated by the math sine and cosine functions, in PHP this sin­Docs (and others linked there).

You can use a loop and that function to prefill the array as well and re-use the array so you have pre-computed values:

$sine = function($v)
{
    return sin($v * 0.5 * M_PI);
};

$dynamic = array();
$base = 255;
for ($i = 0; $i <= $base; $i++)
{
    $dynamic[$i] = $i/$base;
}

$dynamic = array_map($sine, $dynamic);

I use a variable function here, so you can write multiple and can easily test which one matches your needs.

hakre
  • 178,314
  • 47
  • 389
  • 754
  • Thanks hakre, a predefined array would certainly be the simplest solution, the mind boggles as to why I hadn't thought of it myself. If all else fails this is the approach I will take. That said however, I would rather have a simple equation do the work (that can ultimately be modified simply should the need arise) and as such I am currently exploring your sine/cosine suggestion. Thanks again. – gordyr Jan 03 '12 at 15:25
  • For sine it looks like that x goes from i/255 * 0.5 * PI with i from 0-255. Might give you your curve. Will add an example. – hakre Jan 03 '12 at 16:11
  • Fantastic answer hakre many thanks. Your example does exactly what I need. Incidentally I nearly got there using the pow function as suggested by Alnitak. I'm certain that his method would work also but for taking the time to provide an example, you deserve the answer. Huge thanks to all. :-) – gordyr Jan 03 '12 at 16:16
  • No problem, I'm still not sure yet if that equitation in the answer is creating the dynamics you're looking for. You can add your own as well by editing the answer ;) – hakre Jan 03 '12 at 16:22
  • IMHO, the sin curve doesn't provide enough low-end enhancement - unsurprising given that for low x, `sin(x) ~= x` – Alnitak Jan 03 '12 at 17:05
  • FWIW, the same problem is evident in astrophotography which is a hobby of mine - one needs to enhance the difference between low light levels, whilst ignoring differences in high values. Gamma curves are used for that. – Alnitak Jan 03 '12 at 17:08
  • Well, I must admit that sin was a guess by me. I knew it for making natural looking movements back in some scroller. That always looked very dynamic ;) An as I remember right, for speed purposes I used pre-calculated values as well. – hakre Jan 03 '12 at 17:21
  • it's "natural" because in the specific range it's a monotonic function - i.e. _if x1 > x0 then sin(x1) > sin(x0)_. However it's not a suitable function (IMHO) for "signal enhancement" – Alnitak Jan 03 '12 at 17:44
  • After what you outlined I don't think so either. Probably adding some more dynamics would be useful, pow imho often appears not that dynamical, probably a combination of both will give a really good pair. – hakre Jan 03 '12 at 18:05
3

You need something similar to gamma correction.

For input values x in the range 0.0 -> 1.0, take y = pow(x, n) when n should be in the range 0.2 - 0.7 (ish). Just pick a number that gives the desired curve.

As your values are in the range 0 -> 255 you will need to divide by 255.0, apply the pow function, and then multiply by 255 again, e.g.

$y = 255 * pow($x / 255.0, 0.4);

The pow formula satisfies the criteria that 0 and 1 map to themselves, and smaller values are "amplified" more than larger values.

Here's a graph showing gamma curves for n = 1 / 1.6, 1 / 2, 1 / 2.4 and 1 / 2.8, vs the sin curve (in red):

Gamma Curves vs Sin

The lower the value of n, the more "compression" is applied to the low end, so the light blue line is the one with n = 1 / 2.8.

Note how the sin curve is almost linear in the range 0 to 0.5, so provides almost no low end compression at all.

If as I suspect your values are actually centered around 128, then you need to modify the formula somewhat:

$v = ($x - 128.0) / 128.0;
$y = 128 + 127 * sign($v) * pow(abs($v), 0.4);

although I see that the PHP developers have not included a sign function in the PHP library.

Alnitak
  • 313,276
  • 69
  • 379
  • 466
  • Thanks Alnitak, I am currently taking a look at both hakre's and your own suggestion. (sine/cosine & the pow function respectively.) Many thanks for your answer and hopefully I should be able to mark this as answered soon. :-) – gordyr Jan 03 '12 at 15:39
  • This is fantastic info Alnitak. Thank you. It does indeed look like a better overall solution. However for some reason I am having trouble getting it to render the image correctly. There must be something wrong with my implementation of your answer. I'll keep trying and report back. – gordyr Jan 03 '12 at 18:05
  • @gordyr are some of your values negative or maybe based around a zero value of 128? – Alnitak Jan 03 '12 at 18:25
  • no, they should all be between 0 and 255. I have managed to get the waveform to render using the pow method as you suggest by replacing my $v line as above with: $v = (255 * pow($data / 255.0, 0.4)) * $height; however this seems to amplify the low values a huge amount. I will edit my question with an update showing an image of the waveform generated using the pow function. – gordyr Jan 03 '12 at 19:07
  • @gordyr is the mean value 128, though? I've updated my answer to show how to fix that. If the amplification is still too great, tweak the 0.4 value upwards a bit. – Alnitak Jan 03 '12 at 19:12
  • Sorry, the above line of code should read: $v = pow($data / 255.0, 0.4) * $height; – gordyr Jan 03 '12 at 19:15
  • I have edited my question with an image of the waveform that is being generated. I'm trying to get my head around your formula amendment now. My apologies for being slow. Math/formulas of any sort really aren't my strongpoint. – gordyr Jan 03 '12 at 19:26
  • `sign($x) = (($x < 0) ? -1 : (($x > 0) ? 1 : 0))`. If possible please also dump a sample of your raw data somewhere. I can't see how you'd get that graph. – Alnitak Jan 03 '12 at 19:31
  • Thanks Alnitak, I have updated my question again with a raw data dump. – gordyr Jan 03 '12 at 20:23
0

Simple downsampling is not going to give you a correct render, as it will leave the original signal with only low frequencies, whilst all frequencies contribute to amplitudes. So you need to build the peak data (min and max for a certain range of values) from the original waveform to visualize it. You shouldn't apply any non-linear functions to your data, as the waveform representation is linear (unlike gamma-compressed images).

punpcklbw
  • 101
  • 1
  • 5