void GdImageRenderer::drawWaveform(const WaveformBuffer& buffer) const
{
    // Avoid drawing over the right border
    const int max_x = render_axis_labels_ ? image_width_ - 1 : image_width_;

    // Avoid drawing over the top and bottom borders
    const int wave_bottom_y   = render_axis_labels_ ? image_height_ - 2 : image_height_ - 1;
    const int max_wave_height = render_axis_labels_ ? image_height_ - 2 : image_height_;

    const int buffer_size = buffer.getSize();

    // Avoid drawing over the left border
    int x = render_axis_labels_ ? 1 : 0;
    int i = render_axis_labels_ ? start_index_ + 1 : start_index_;

    for (; x < max_x && i < buffer_size; ++i, ++x) {
        // convert range [-32768, 32727] to [0, 65535]
        int low  = buffer.getMinSample(i) + 32768;
        int high = buffer.getMaxSample(i) + 32768;

        // scale to fit the bitmap
        int low_y  = wave_bottom_y - low  * max_wave_height / 65536;
        int high_y = wave_bottom_y - high * max_wave_height / 65536;

        gdImageLine(image_, x, low_y, x, high_y, waveform_color_);
    }
}
static std::pair<int, int> getAmplitudeRange(
    const WaveformBuffer& buffer,
    int start_index,
    int end_index)
{
    int low  = std::numeric_limits<int>::max();
    int high = std::numeric_limits<int>::min();

    const int channels = buffer.getChannels();

    for (int i = start_index; i != end_index; ++i) {
        for (int channel = 0; channel < channels; ++channel) {
            const int min = buffer.getMinSample(channel, i);
            const int max = buffer.getMaxSample(channel, i);

            if (min < low) {
                low = min;
            }

            if (max > high) {
                high = max;
            }
        }
    }

    return std::make_pair(low, high);
}
bool OptionHandler::convertWaveformData(
    const boost::filesystem::path& input_filename,
    const boost::filesystem::path& output_filename,
    const Options& options)
{
    WaveformBuffer buffer;

    if (!buffer.load(input_filename.c_str())) {
        return false;
    }

    const int bits = options.hasBits() ? options.getBits() : buffer.getBits();

    bool success = true;

    const boost::filesystem::path output_file_ext = output_filename.extension();

    if (output_file_ext == ".json") {
        success = buffer.saveAsJson(output_filename.c_str(), bits);
    }
    else if (output_file_ext == ".txt") {
        success = buffer.saveAsText(output_filename.c_str(), bits);
    }

    return success;
}
TEST_F(WaveformGeneratorTest, shouldComputeMaxAndMinValuesFromStereoInput)
{
    WaveformBuffer buffer;

    const int samples_per_pixel = 300;

    SamplesPerPixelScaleFactor scale_factor(samples_per_pixel);
    WaveformGenerator generator(buffer, scale_factor);

    const int sample_rate = 44100;
    const int channels    = 2;
    const int BUFFER_SIZE = 1024;

    short samples[BUFFER_SIZE];
    memset(samples, 0, sizeof(samples));

    const int frames = BUFFER_SIZE / channels;

    bool result = generator.init(sample_rate, channels, BUFFER_SIZE);

    ASSERT_TRUE(result);
    ASSERT_TRUE(error.str().empty());

    // even indexes: left channel, odd indexes: right channel
    samples[0] = 100;
    samples[1] = 102;
    samples[200] = 98;
    samples[201] = 100;
    samples[400] = -98;
    samples[401] = -100;
    samples[598] = -100;
    samples[599] = -102;

    samples[600] = 197;
    samples[601] = 199;
    samples[800] = -200;
    samples[801] = -202;
    samples[900] = -197;
    samples[901] = -199;
    samples[1022] = 200;
    samples[1023] = 202;

    result = generator.process(samples, frames);
    ASSERT_TRUE(result);

    generator.done();

    // Check contents of buffer
    ASSERT_THAT(buffer.getSampleRate(), Eq(44100));
    ASSERT_THAT(buffer.getSamplesPerPixel(), Eq(300));
    ASSERT_THAT(buffer.getSize(), Eq(2)); // 512 / 300 = 1 remainder 212
                                          // => 2 output points total

    // Check min and max values are average of left and right channels
    ASSERT_THAT(buffer.getMinSample(0), Eq(-101));
    ASSERT_THAT(buffer.getMaxSample(0), Eq(101));

    ASSERT_THAT(buffer.getMinSample(1), Eq(-201));
    ASSERT_THAT(buffer.getMaxSample(1), Eq(201));
}
void GdImageRendererTest::testImageRendering(bool axis_labels)
{
    const char* filename = temp_filename_.getFilename();
    ASSERT_NE(nullptr, filename);

    // Ensure temporary file is deleted at end of test.
    FileDeleter deleter(filename);

    bool result = buffer_.load("../test/data/test_file_stereo_8bit_64spp.dat");
    ASSERT_TRUE(result);

    const WaveformColors& colors = audacityWaveformColors;

    result = renderer_.create(buffer_, 5.0, 1000, 300, colors, axis_labels); // zoom: 128
    ASSERT_TRUE(result);

    result = renderer_.saveAsPng(filename);
    ASSERT_TRUE(result);

    struct stat info;
    int stat_result = stat(filename, &info);

    ASSERT_THAT(stat_result, Eq(0));
    ASSERT_THAT(info.st_size, Gt(0));

    ASSERT_FALSE(output.str().empty());
    ASSERT_TRUE(error.str().empty());
}
TEST_F(WaveformGeneratorTest, shouldComputeMaxAndMinValuesFromMonoInput)
{
    WaveformBuffer buffer;

    const int samples_per_pixel = 300;

    SamplesPerPixelScaleFactor scale_factor(samples_per_pixel);
    WaveformGenerator generator(buffer, scale_factor);

    const int sample_rate = 44100;
    const int channels    = 1;
    const int BUFFER_SIZE = 512;

    short samples[BUFFER_SIZE];
    memset(samples, 0, sizeof(samples));

    const int frames = BUFFER_SIZE / channels;

    bool result = generator.init(sample_rate, channels, BUFFER_SIZE);

    ASSERT_TRUE(result);
    ASSERT_TRUE(error.str().empty());

    // samples for first waveform data point
    samples[0] = 100;
    samples[100] = 98;
    samples[200] = -98;
    samples[299] = -102;

    // samples for second waveform data point
    samples[300] = 197;
    samples[400] = -200;
    samples[450] = -197;
    samples[511] = 202;

    result = generator.process(samples, frames);
    ASSERT_TRUE(result);

    generator.done();

    // Check contents of buffer
    ASSERT_THAT(buffer.getSampleRate(), Eq(44100));
    ASSERT_THAT(buffer.getSamplesPerPixel(), Eq(300));
    ASSERT_THAT(buffer.getSize(), Eq(2)); // 512 / 300 = 1 remainder 212
                                          // => 2 output points total

    // Check min and max values
    ASSERT_THAT(buffer.getMinSample(0), Eq(-102));
    ASSERT_THAT(buffer.getMaxSample(0), Eq(100));

    ASSERT_THAT(buffer.getMinSample(1), Eq(-200));
    ASSERT_THAT(buffer.getMaxSample(1), Eq(202));
}
TEST_F(WaveformGeneratorTest, shouldSetBufferAttributes)
{
    WaveformBuffer buffer;

    const int samples_per_pixel = 300;

    WaveformGenerator generator(buffer, samples_per_pixel);

    const int sample_rate = 44100;
    const int channels    = 2;
    const int BUFFER_SIZE = 1024;

    bool result = generator.init(sample_rate, channels, BUFFER_SIZE);

    ASSERT_TRUE(result);

    ASSERT_THAT(buffer.getSampleRate(), Eq(44100));
    ASSERT_THAT(buffer.getSamplesPerPixel(), Eq(300));
}
TEST_F(WaveformGeneratorTest, shouldSucceedIfEndTimeGreaterThanStartTime)
{
    WaveformBuffer buffer;
    DurationScaleFactor scale_factor(2.0, 3.0, 100);

    WaveformGenerator generator(buffer, scale_factor);

    const int sample_rate = 44100;
    const int channels    = 2;
    const int BUFFER_SIZE = 1024;

    bool result = generator.init(sample_rate, channels, BUFFER_SIZE);

    ASSERT_TRUE(result);
    ASSERT_TRUE(error.str().empty());

    ASSERT_THAT(generator.getSamplesPerPixel(), Eq(441));
    ASSERT_THAT(buffer.getSampleRate(), Eq(44100));
    ASSERT_THAT(buffer.getSamplesPerPixel(), Eq(441));
}
bool OptionHandler::generateWaveformData(
    const boost::filesystem::path& input_filename,
    const boost::filesystem::path& output_filename,
    const Options& options)
{
    const std::unique_ptr<ScaleFactor> scale_factor = createScaleFactor(options);

    const boost::filesystem::path output_file_ext = output_filename.extension();

    const std::unique_ptr<AudioFileReader> audio_file_reader =
        createAudioFileReader(input_filename);

    if (audio_file_reader == nullptr) {
        error_stream << "Unknown file type: " << input_filename << '\n';
        return false;
    }

    if (!audio_file_reader->open(input_filename.c_str())) {
        return false;
    }

    WaveformBuffer buffer;
    WaveformGenerator processor(buffer, *scale_factor);

    if (!audio_file_reader->run(processor)) {
        return false;
    }

    assert(output_file_ext == ".dat" || output_file_ext == ".json");

    const int bits = options.getBits();

    if (output_file_ext == ".dat") {
        return buffer.save(output_filename.c_str(), bits);
    }
    else {
        return buffer.saveAsJson(output_filename.c_str(), bits);
    }
}
void GdImageRenderer::drawWaveform(const WaveformBuffer& buffer) const
{
    const int max_x = image_width_ - 1;

    const int wave_bottom_y   = image_height_ - 2;
    const int max_wave_height = image_height_ - 3;

    const int buffer_size = buffer.getSize();

    int x = 1; // Avoid drawing over the left border
    int i = start_index_ + 1;

    for (; x < max_x && i < buffer_size; ++i, ++x) {
        // convert range [-32768, 32727] to [0, 65535]
        int low  = buffer.getMinSample(i) + 32768;
        int high = buffer.getMaxSample(i) + 32768;

        // scale to fit the bitmap
        int low_y  = wave_bottom_y - low  * max_wave_height / 65536;
        int high_y = wave_bottom_y - high * max_wave_height / 65536;

        gdImageLine(image_, x, low_y, x, high_y, wave_color_);
    }
}
bool GdImageRenderer::create(
    const WaveformBuffer& buffer,
    const double start_time,
    const int image_width,
    const int image_height,
    const WaveformColors& colors,
    const bool render_axis_labels)
{
    if (start_time < 0.0) {
        error_stream << "Invalid start time: minimum 0\n";
        return false;
    }
    else if (start_time > MAX_START_TIME) {
        error_stream << "Invalid start time: maximum " << MAX_START_TIME << '\n';
        return false;
    }

    if (image_width < 1) {
        error_stream << "Invalid image width: minimum 1\n";
        return false;
    }

    if (image_height < 1) {
        error_stream << "Invalid image height: minimum 1\n";
        return false;
    }

    const int sample_rate = buffer.getSampleRate();

    if (sample_rate > MAX_SAMPLE_RATE) {
        error_stream << "Invalid sample rate: " << sample_rate
                     << " Hz, maximum " << MAX_SAMPLE_RATE << " Hz\n";
        return false;
    }

    const int samples_per_pixel = buffer.getSamplesPerPixel();

    if (samples_per_pixel > MAX_ZOOM) {
        error_stream << "Invalid zoom: maximum " << MAX_ZOOM << '\n';
        return false;
    }

    image_ = gdImageCreateTrueColor(image_width, image_height);

    if (image_ == nullptr) {
        error_stream << "Failed to create image\n";
        return false;
    }

    assert(sample_rate != 0);
    assert(samples_per_pixel != 0);

    image_width_        = image_width;
    image_height_       = image_height;
    start_time_         = start_time;
    sample_rate_        = buffer.getSampleRate();
    samples_per_pixel_  = samples_per_pixel;
    start_index_        = secondsToPixels(start_time);
    render_axis_labels_ = render_axis_labels;

    output_stream << "Image dimensions: " << image_width_ << "x" << image_height_ << " pixels"
                  << "\nSample rate: " << sample_rate_ << " Hz"
                  << "\nSamples per pixel: " << samples_per_pixel_
                  << "\nStart time: " << start_time_ << " seconds"
                  << "\nStart index: " << start_index_
                  << "\nBuffer size: " << buffer.getSize()
                  << "\nAxis labels: " << (render_axis_labels_ ? "yes" : "no") << std::endl;

    if (colors.hasAlpha()) {
        gdImageSaveAlpha(image_, 1);
        gdImageAlphaBlending(image_, 0);
    }

    initColors(colors);
    drawBackground();

    if (render_axis_labels_) {
        drawBorder();
    }

    drawWaveform(buffer);

    if (render_axis_labels_) {
        drawTimeAxisLabels();
    }

    return true;
}
void GdImageRenderer::drawWaveform(const WaveformBuffer& buffer) const
{
    // Avoid drawing over the right border
    const int max_x = render_axis_labels_ ? image_width_ - 1 : image_width_;

    // Avoid drawing over the top and bottom borders
    const int top_y = render_axis_labels_ ? 1 : 0;
    const int bottom_y = render_axis_labels_ ? image_height_ - 2 : image_height_ - 1;

    const int buffer_size = buffer.getSize();

    // Avoid drawing over the left border
    const int start_x     = render_axis_labels_ ? 1 : 0;
    const int start_index = render_axis_labels_ ? start_index_ + 1 : start_index_;

    double amplitude_scale;

    if (auto_amplitude_scale_) {
        int end_index = start_index + max_x;

        if (end_index > buffer_size) {
            end_index = buffer_size;
        }

        std::pair<int, int> range = getAmplitudeRange(buffer, start_index, end_index);

        double amplitude_scale_high = (range.second == 0) ? 1.0 : 32767.0 / range.second;
        double amplitude_scale_low  = (range.first  == 0) ? 1.0 : 32767.0 / range.first;

        amplitude_scale = std::fabs(std::min(amplitude_scale_high, amplitude_scale_low));
    }
    else {
        amplitude_scale = amplitude_scale_;
    }

    output_stream << "Amplitude scale: " << amplitude_scale << '\n';

    const int channels = buffer.getChannels();

    int available_height = bottom_y - top_y + 1;

    const int row_height = available_height / channels;

    int waveform_top_y = render_axis_labels_ ? 1 : 0;

    for (int channel = 0; channel < channels; ++channel) {
        int waveform_bottom_y;

        if (channel == channels - 1) {
            waveform_bottom_y = waveform_top_y + available_height - 1;
        }
        else {
            waveform_bottom_y = waveform_top_y + row_height;
        }

        const int height = waveform_bottom_y - waveform_top_y + 1;

        for (int i = start_index, x = start_x; x < max_x && i < buffer_size; ++i, ++x) {
            // Convert range [-32768, 32727] to [0, 65535]
            int low  = scale(buffer.getMinSample(channel, i), amplitude_scale) + 32768;
            int high = scale(buffer.getMaxSample(channel, i), amplitude_scale) + 32768;

            // Scale to fit the bitmap
            int high_y = waveform_top_y + height - 1 - high * height / 65536;
            int low_y  = waveform_top_y + height - 1 - low  * height / 65536;

            gdImageLine(image_, x, low_y, x, high_y, waveform_color_);
        }

        available_height -= row_height + 1;
        waveform_top_y += row_height + 1;
    }
}