Displaying the Episode's Waveform

What’s a Waveform?

A waveform is a visual representation of an audio file according to its volume intensity changes over time. Spreaker uses a waveform to display an episode’s playback progress on both the website and widget, and provides access to the waveform data points through the API.

Spreaker Live Show

Loading Waveform Data Points

The waveform data consists of a JSON-encoded array of 870 float numbers ranging from 0 (lowest) to 1 (highest). Each Episode exports the property waveform_url, which contains the URL of a gzipped JSON, and within that contains the episode’s waveform data points.

Example: Get an episode’s waveform URL

curl -s https://api.spreaker.com/v2/episodes/550 | json -a response.episode.waveform_url

Example: Get an episode’s waveform data points

curl --compressed https://d3770qakewhkht.cloudfront.net/episode_550.gz.json

The response body is a JSON object containing every waveform data point.

{
    "response": {
        "points": [0.47449, 0.936584, 0.383244, ..., 0.000393, 0]
    }
}

## Drawing the Waveform

Drawing the waveform is quite easy. Given that H is the height of your waveform, you should draw a vertical line for each data point, where each line should be a maximum height of H * data_point_value. The waveform lines should be anchored to the bottom of the waveform area.

Note: the widget’s waveform, embedded at the top of the page, draws 2 lines (data points) every 3 pixels. The natural total width of that waveform is 870 + (870 / 3) pixels = 1160 pixels. In order to fit the waveform in a different area, you should rescale the data points (see the next section).

Rescaling Waveform Data Points

Spreaker provides 870 data points for each waveform. However, in some cases you may need more or fewer. For the sake of simplicity, we provide a PHP implementation of an algorithm that rescales data points from 870 values to the number of values you need.

<?php

function _interpolate($points, $num_out_points)
{
    $num_in_points = count($points);

    // Edge cases
    if ($num_out_points == $num_in_points) {
        return $points;
    } else if ($num_out_points <= 0) {
        return array();
    } else if ($num_out_points == 1) {
        return array(round(array_sum($points) / $num_in_points, 6));
    } else if ($num_in_points == 1) {
        return array_fill(0, $num_out_points, round($points[0], 6));
    }

    $output = array_fill(0, $num_out_points, 0);

    // Stretch
    if ($num_out_points > $num_in_points)
    {
        $delta = $num_in_points  / $num_out_points;

        for ($i = 0; $i < $num_out_points; $i++) {
            $interValue = $i * $delta;
            $prev       = floor($interValue);
            $next       = $prev + 1;
            $d          = $interValue - $prev;

            // taking care of the last sample (next == $num_in_points)
            if ($next == $num_in_points) {
                $value = $points[$prev];
            } else {
                $value = (1 - $d) * $points[$prev] + $d * $points[$next];
            }

            $output[$i] = $value;
        }
    }
    // Reduce
    else
    {
        // Init
        $spanForNewSamples    = $num_in_points / $num_out_points;
        $j                    = 0;
        $leftOverFromPrevious = 0;

        // Reduce (algorithm implemented by Dema)
        for ($i = 0; $i < $num_out_points; $i++) {
            $upTo        = ($i + 1) * $spanForNewSamples;
            $toJ         = floor($upTo);
            $rest        = $upTo - $toJ;
            $accumulator = $leftOverFromPrevious;

            // accumulating old samples that fall ENTIRELY on the span
            while ($j < $toJ) {
                $accumulator += $points[$j];
                $j++;
            }

            // for old samples in between two spans accumulating on the current new sample only the relevant span (rest * value)
            // and leaving the remaining (1-rest) on the subsequent new sample
            if ($rest > 0) {
                if ($j < $num_in_points - 1) {
                    $j++;
                    $accumulator += $points[$j] * $rest;
                    $leftOverFromPrevious = $points[$j] * (1 - $rest);
                }
            } else {
                $leftOverFromPrevious = 0;
            }

            // All merged points MUST be on the same scale, so / $spanForNewSamples
            $output[$i] = $accumulator / $spanForNewSamples;
        }
    }

    return $output;
}