Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rendering waveform in PHP - How to produce a more compressed render?

Tags:

php

math

libpng

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.

like image 911
gordyr Avatar asked Jan 03 '12 15:01

gordyr


2 Answers

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.

like image 152
hakre Avatar answered Oct 22 '22 18:10

hakre


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.

like image 25
Alnitak Avatar answered Oct 22 '22 17:10

Alnitak