/* === S Y N F I G ========================================================= */
/*! \file widgets/widget_soundwave.h
** \brief Widget for display a sound wave time-graph
**
** $Id$
**
** \legal
** Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
** ......... ... 2019 Rodolfo Ribeiro Gomes
**
** This package is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public License as
** published by the Free Software Foundation; either version 2 of
** the License, or (at your option) any later version.
**
** This package is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
** General Public License for more details.
** \endlegal
*/
#ifdef USING_PCH
# include "pch.h"
#else
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include "widget_soundwave.h"
#include <synfig/general.h>
#include <gui/canvasview.h>
#include <gui/timeplotdata.h>
#include <gui/localization.h>
#include <Mlt.h>
#include <cairomm/cairomm.h>
#include <gdkmm.h>
#include <glibmm/convert.h>
#include <cstring>
#endif
using namespace studio;
const int default_frequency = 48000;
const int default_n_channels = 2;
Widget_SoundWave::MouseHandler::~MouseHandler() {}
Widget_SoundWave::Widget_SoundWave()
: Widget_TimeGraphBase(),
frequency(default_frequency),
n_channels(default_n_channels),
n_samples(0),
channel_idx(0),
loading_error(false)
{
add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::SCROLL_MASK | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK | Gdk::KEY_RELEASE_MASK);
setup_mouse_handler();
set_default_page_size(255);
set_zoom(1.0);
int range_max = 255;
int range_min = 0;
int h = get_allocated_height();
ConfigureAdjustment(range_adjustment)
.set_lower(-range_max /*- 0.05*range_adjustment->get_page_size()*/)
.set_upper(-range_min /*+ 0.05*range_adjustment->get_page_size()*/)
.set_step_increment(range_adjustment->get_page_size()*20.0/h) // 20 pixels
.set_value((range_max-range_min)/2)
.finish();
}
Widget_SoundWave::~Widget_SoundWave()
{
clear();
}
bool Widget_SoundWave::load(const std::string& filename)
{
clear();
std::lock_guard<std::mutex> lock(mutex);
if (!do_load(filename)) {
loading_error = true;
queue_draw();
return false;
}
this->filename = filename;
signal_file_loaded().emit(filename);
queue_draw();
return true;
}
void Widget_SoundWave::clear()
{
std::lock_guard<std::mutex> lock(mutex);
buffer.clear();
this->filename.clear();
loading_error = false;
sound_delay = 0.0;
channel_idx = 0;
n_samples = 0;
queue_draw();
}
void Widget_SoundWave::set_channel_idx(int new_channel_idx)
{
if (channel_idx != new_channel_idx && new_channel_idx >= 0 && new_channel_idx < n_channels) {
channel_idx = new_channel_idx;
queue_draw();
}
}
int Widget_SoundWave::get_channel_idx() const
{
return channel_idx;
}
int Widget_SoundWave::get_channel_number() const
{
return n_channels;
}
void Widget_SoundWave::set_delay(synfig::Time delay)
{
if (delay == sound_delay)
return;
std::lock_guard<std::mutex> lock(mutex);
sound_delay = delay;
buffer.clear();
n_samples = 0;
do_load(filename);
queue_draw();
}
const synfig::Time& Widget_SoundWave::get_delay() const
{
return sound_delay;
}
bool Widget_SoundWave::on_event(GdkEvent* event)
{
if (mouse_handler.process_event(event))
return true;
return Widget_TimeGraphBase::on_event(event);
}
bool Widget_SoundWave::on_draw(const Cairo::RefPtr<Cairo::Context>& cr)
{
if (Widget_TimeGraphBase::on_draw(cr))
return true;
if (loading_error) {
Glib::RefPtr<Pango::Layout> layout(Pango::Layout::create(get_pango_context()));
layout->set_text(_("Audio file not supported"));
cr->save();
cr->set_source_rgb(0.8, 0.3, 0.3);
cr->move_to(5, get_height()/2);
layout->show_in_cairo_context(cr);
cr->restore();
}
if (filename.empty())
return true;
if (buffer.empty())
return true;
cr->save();
std::lock_guard<std::mutex> lock(mutex);
const int bytes_per_sample = 1;
Gdk::RGBA color = get_style_context()->get_color();
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
const int middle_y = 127;
cr->move_to(0, middle_y);
const int stride = frequency * bytes_per_sample * n_channels;
for (double x = 0; x < get_width(); x+=0.1) {
synfig::Time t = time_plot_data->get_t_from_pixel_coord(x);
if (synfig::approximate_greater(synfig::Real(sound_delay), 0.0))
t = t - sound_delay;
synfig::Time dt = t - time_plot_data->time_model->get_lower();
int index = int(dt * stride) + channel_idx;
int value = middle_y;
if (index >= 0 && index < buffer.size())
std::copy(buffer.begin() + index, buffer.begin() + index + bytes_per_sample, &value);
int y = time_plot_data->get_pixel_y_coord(value);
cr->line_to(x, y);
}
cr->set_line_width(0.3);
cr->stroke();
draw_current_time(cr);
int h = get_height();
ConfigureAdjustment(range_adjustment)
.set_step_increment(range_adjustment->get_page_size()*20.0/h) // 20 pixels
.finish();
cr->restore();
return true;
}
void Widget_SoundWave::set_time_model(const etl::handle<TimeModel>& x)
{
if (x) {
previous_lower_time = x->get_lower();
previous_upper_time = x->get_upper();
}
Widget_TimeGraphBase::set_time_model(x);
}
void Widget_SoundWave::on_time_model_changed()
{
if (filename.empty())
return;
if (previous_lower_time == time_plot_data->time_model->get_lower()
&& previous_upper_time == time_plot_data->time_model->get_upper())
return;
previous_lower_time = time_plot_data->time_model->get_lower();
previous_upper_time = time_plot_data->time_model->get_upper();
std::lock_guard<std::mutex> lock(mutex);
buffer.clear();
n_samples = 0;
do_load(filename);
queue_draw();
}
void Widget_SoundWave::setup_mouse_handler()
{
mouse_handler.set_pan_enabled(true);
mouse_handler.set_zoom_enabled(true);
mouse_handler.set_scroll_enabled(true);
mouse_handler.set_canvas_interface(canvas_interface);
mouse_handler.signal_redraw_needed().connect(sigc::mem_fun(*this, &Gtk::Widget::queue_draw));
mouse_handler.signal_focus_requested().connect(sigc::mem_fun(*this, &Gtk::Widget::grab_focus));
mouse_handler.signal_zoom_in_requested().connect(sigc::mem_fun(*this, &Widget_SoundWave::zoom_in));
mouse_handler.signal_zoom_out_requested().connect(sigc::mem_fun(*this, &Widget_SoundWave::zoom_out));
mouse_handler.signal_scroll_up_requested().connect(sigc::mem_fun(*this, &Widget_SoundWave::scroll_up));
mouse_handler.signal_scroll_down_requested().connect(sigc::mem_fun(*this, &Widget_SoundWave::scroll_down));
mouse_handler.signal_panning_requested().connect(sigc::mem_fun(*this, &Widget_SoundWave::pan));
}
bool Widget_SoundWave::do_load(const std::string& filename)
{
std::string real_filename = Glib::filename_from_uri(filename);
Mlt::Profile profile;
Mlt::Producer *track = new Mlt::Producer(profile, (std::string("avformat:") + real_filename).c_str());
if (!track->get_producer() || track->get_length() <= 0) {
delete track;
track = new Mlt::Producer(profile, (std::string("vorbis:") + real_filename).c_str());
if (!track->get_producer() || track->get_length() <= 0) {
delete track;
return false;
}
}
const int length = track->get_length();
double fps = track->get_fps();
int start_frame = (time_plot_data->time_model->get_lower() - sound_delay) * fps;
int end_frame = (time_plot_data->time_model->get_upper() - sound_delay) * fps;
start_frame = synfig::clamp(start_frame, 0, length);
end_frame = synfig::clamp(end_frame, 0, length);
track->seek(start_frame);
// check if audio is seekable
if (track->position() != start_frame) {
// Not seekable!
synfig::error("Audio file not seekable, but a delay (%s) was set: %s", sound_delay.get_string(time_plot_data->time_model->get_frame_rate()).c_str(), filename.c_str());
}
unsigned char *outbuffer = nullptr;
int bytes_written = 0;
for (int i = start_frame; i < end_frame; ++i) {
Mlt::Frame *frame = track->get_frame(0);
if (!frame)
break;
mlt_audio_format format = mlt_audio_u8;
int bytes_per_sample = 1;
int _frequency = frequency? frequency : default_frequency;
int _channels = n_channels ? n_channels : default_n_channels;
int _n_samples = 0;
void * _buffer = frame->get_audio(format, _frequency, _channels, _n_samples);
if (_buffer == nullptr) {
delete frame;
break;
}
if (buffer.empty()) {
int buffer_length = (end_frame - start_frame + 1) * _channels * bytes_per_sample * std::round(_frequency/fps);
buffer.resize(buffer_length);
}
int _n_bytes = _n_samples * _channels * bytes_per_sample;
std::copy(static_cast<unsigned char*>(_buffer), static_cast<unsigned char*>(_buffer) + _n_bytes, buffer.begin()+bytes_written);
bytes_written += _n_bytes;
outbuffer += _n_bytes;
frequency = _frequency;
n_channels = _channels;
n_samples += _n_samples;
delete frame;
}
if (channel_idx > n_channels)
channel_idx = 0;
queue_draw();
delete track;
return true;
}