diff --git a/libs/ardour/ardour/ffmpegfileimportable.h b/libs/ardour/ardour/ffmpegfileimportable.h new file mode 100644 index 0000000000..b14e8c1a5b --- /dev/null +++ b/libs/ardour/ardour/ffmpegfileimportable.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 Marijn Kruisselbrink + * + * This program 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 program 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. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _ardour_ffmpegfile_importable_source_h_ +#define _ardour_ffmpegfile_importable_source_h_ + +#include "pbd/g_atomic_compat.h" +#include "pbd/ringbuffer.h" + +#include "ardour/importable_source.h" +#include "ardour/libardour_visibility.h" +#include "ardour/system_exec.h" +#include "ardour/types.h" + +namespace ARDOUR { + +class LIBARDOUR_API FFMPEGFileImportableSource : public ImportableSource { +public: + enum { + ALL_CHANNELS = -1, + }; + FFMPEGFileImportableSource(const std::string &path, int channel = ALL_CHANNELS); + virtual ~FFMPEGFileImportableSource(); + + /* ImportableSource API */ + uint32_t channels () const { return _channels; } + samplecnt_t length () const { return _length; } + samplecnt_t samplerate () const { return _samplerate; } + samplepos_t natural_position () const { return _natural_position; } + void seek (samplepos_t pos); + samplecnt_t read (Sample*, samplecnt_t nframes); + + bool clamped_at_unity () const { return false; } + + std::string format_name () const { return _format_name; } + +private: + void start_ffmpeg (); + void reset (); + + void did_read_data (std::string data, size_t size); + + std::string _path; + int _channel; + + uint32_t _channels; + samplecnt_t _length; + samplecnt_t _samplerate; + samplepos_t _natural_position; + std::string _format_name; + + PBD::RingBuffer _buffer; + // Set to 1 to indicate that ffmpeg should be terminating. + GATOMIC_QUAL gint _ffmpeg_should_terminate; + + // To make sure we don't try to parse partial floats, we might have a couple of bytes + // of leftover unparsable data after any `did_read_data` call. Those couple of bytes are + // stored here until the next `did_read_data` call. + std::string _leftover_data; + + samplecnt_t _read_pos; + + ARDOUR::SystemExec *_ffmpeg_exec; + PBD::ScopedConnection _ffmpeg_conn; +}; + +} + +#endif /* _ardour_ffmpegfile_importable_source_h_ */ diff --git a/libs/ardour/ardour/ffmpegfilesource.h b/libs/ardour/ardour/ffmpegfilesource.h new file mode 100644 index 0000000000..6ff891fc69 --- /dev/null +++ b/libs/ardour/ardour/ffmpegfilesource.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 Marijn Kruisselbrink + * + * This program 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 program 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. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _ardour_ffmpegfile_source_h_ +#define _ardour_ffmpegfile_source_h_ + +#include + +#include "ardour/audiofilesource.h" +#include "ardour/ffmpegfileimportable.h" + +namespace ARDOUR { + +class LIBARDOUR_API FFMPEGFileSource : public AudioFileSource { +public: + FFMPEGFileSource(ARDOUR::Session &, const std::string &path, int chn, Flag); + ~FFMPEGFileSource(); + + /* AudioSource API */ + float sample_rate() const { return _ffmpeg.samplerate (); } + bool clamped_at_unity () const { return false; } + + /* AudioFileSource API */ + void flush () {} + int update_header (samplepos_t when, struct tm&, time_t) { return 0; } + int flush_header () { return 0; } + void set_header_natural_position () {}; + + static int get_soundfile_info (const std::string& path, SoundFileInfo& _info, std::string& error_msg); + static bool safe_audio_file_extension (const std::string &file); + +protected: + /* FileSource API */ + void close (); + /* AudioSource API */ + samplecnt_t read_unlocked (Sample *dst, samplepos_t start, samplecnt_t cnt) const; + samplecnt_t write_unlocked (Sample *, samplecnt_t) { return 0; } + +private: + mutable FFMPEGFileImportableSource _ffmpeg; + int _channel; +}; + +} +#endif diff --git a/libs/ardour/audiofilesource.cc b/libs/ardour/audiofilesource.cc index 29e02ff197..969667ddba 100644 --- a/libs/ardour/audiofilesource.cc +++ b/libs/ardour/audiofilesource.cc @@ -51,6 +51,7 @@ #include "ardour/audiofilesource.h" #include "ardour/debug.h" +#include "ardour/ffmpegfilesource.h" #include "ardour/mp3filesource.h" #include "ardour/sndfilesource.h" #include "ardour/session.h" @@ -208,6 +209,10 @@ AudioFileSource::get_soundfile_info (const string& path, SoundFileInfo& _info, s return true; } + if (FFMPEGFileSource::get_soundfile_info (path, _info, error_msg) == 0) { + return true; + } + return false; } @@ -342,7 +347,6 @@ AudioFileSource::safe_audio_file_extension(const string& file) ".mpeg", ".MPEG", ".mp1", ".MP1", ".mp4", ".MP4", - ".m4a", ".M4A", ".sd2", ".SD2", // libsndfile supports sd2 also, but the resource fork is required to open. #endif // HAVE_COREAUDIO }; @@ -353,6 +357,10 @@ AudioFileSource::safe_audio_file_extension(const string& file) } } + if (FFMPEGFileSource::safe_audio_file_extension(file)) { + return true; + } + return false; } diff --git a/libs/ardour/ffmpegfileimportable.cc b/libs/ardour/ffmpegfileimportable.cc new file mode 100644 index 0000000000..34b6d5304e --- /dev/null +++ b/libs/ardour/ffmpegfileimportable.cc @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2021 Marijn Kruisselbrink + * + * This program 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 program 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. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include + +#include "pbd/error.h" +#include "pbd/compose.h" +#include "pbd/i18n.h" + +#include "ardour/ffmpegfileimportable.h" +#include "ardour/filesystem_paths.h" +#include "ardour/system_exec.h" + +namespace ARDOUR { + +static void +receive_stdout (std::string* out, const std::string& data, size_t size) { + *out += data; +} + +FFMPEGFileImportableSource::FFMPEGFileImportableSource(const std::string &path, int channel) + : _path (path) + , _channel (channel) + , _buffer (32768) + , _ffmpeg_should_terminate (0) + , _read_pos (0) + , _ffmpeg_exec (nullptr) +{ + std::string ffprobe_exe, unused; + if (!ArdourVideoToolPaths::transcoder_exe (unused, ffprobe_exe)) { + PBD::error << "FFMPEGFileImportableSource: Can't find ffprobe and ffmpeg" << endmsg; + throw failed_constructor(); + } + + int a = 0; + char **argp = (char **)calloc(10, sizeof(char *)); + argp[a++] = strdup (ffprobe_exe.c_str ()); + argp[a++] = strdup (_path.c_str ()); + argp[a++] = strdup ("-show_streams"); + argp[a++] = strdup ("-of"); + argp[a++] = strdup ("json"); + + auto exec = std::make_unique(ffprobe_exe, argp); + PBD::info << "Probe command: { " << exec->to_s () << "}" << endmsg; + + if (exec->start ()) { + PBD::error << "FFMPEGFileImportableSource: External decoder (ffprobe) cannot be started." << endmsg; + throw failed_constructor(); + } + + try { + PBD::ScopedConnection c; + std::string ffprobe_output; + exec->ReadStdout.connect_same_thread (c, boost::bind (&receive_stdout, &ffprobe_output, _1, _2)); + while (exec->is_running ()) { + // wait for system exec to terminate + Glib::usleep (1000); + } + + namespace pt = boost::property_tree; + std::istringstream is (ffprobe_output); + pt::ptree root; + pt::read_json (is, root); + + // TODO: Find the stream with the most channels, rather than whatever the first one is. + _channels = root.get ("streams..channels"); + _length = root.get ("streams..duration_ts"); + _samplerate = root.get ("streams..sample_rate"); + _natural_position = root.get ("streams..start_pts"); + _format_name = root.get ("streams..codec_long_name"); + } catch (...) { + PBD::error << "FFMPEGFileImportableSource: Failed to read file metadata" << endmsg; + throw failed_constructor(); + } + + if (_channel != ALL_CHANNELS && (_channel < 0 || _channel > (int) channels ())) { + PBD::error << string_compose("FFMPEGFileImportableSource: file only contains %1 channels; %2 is invalid as a channel number", channels(), _channel) << endmsg; + throw failed_constructor(); + } +} + +FFMPEGFileImportableSource::~FFMPEGFileImportableSource () +{ + reset(); +} + +void +FFMPEGFileImportableSource::seek (samplepos_t pos) +{ + if (pos < _read_pos) { + reset(); + } + + if (!_ffmpeg_exec) { + start_ffmpeg(); + } + + while (_read_pos < pos) { + guint read_space = _buffer.read_space (); + if (read_space == 0) { + if (!_ffmpeg_exec->is_running ()) { + // FFMPEG quit, must have reached EOF. + PBD::warning << string_compose ("FFMPEGFileImportableSource: Reached EOF while trying to seek to %1", pos) << endmsg; + break; + } + // TODO: don't just spin, but use some signalling + Glib::usleep (1000); + continue; + } + guint inc = std::min(read_space, pos - _read_pos); + _buffer.increment_read_idx (inc); + _read_pos += inc; + } +} + +samplecnt_t +FFMPEGFileImportableSource::read (Sample* dst, samplecnt_t nframes) +{ + if (!_ffmpeg_exec) { + start_ffmpeg(); + } + + samplecnt_t total_read = 0; + while (nframes > 0) { + guint read = _buffer.read (dst + total_read, nframes); + if (read == 0) { + if (!_ffmpeg_exec->is_running ()) { + // FFMPEG quit, must have reached EOF. + break; + } + // TODO: don't just spin, but use some signalling + Glib::usleep (1000); + continue; + } + nframes -= read; + total_read += read; + _read_pos += read; + } + + return total_read; +} + +void +FFMPEGFileImportableSource::start_ffmpeg () +{ + std::string ffmpeg_exe, unused; + ArdourVideoToolPaths::transcoder_exe (ffmpeg_exe, unused); + + int a = 0; + char **argp = (char **)calloc(16, sizeof(char *)); + char tmp[32]; + argp[a++] = strdup (ffmpeg_exe.c_str ()); + argp[a++] = strdup ("-nostdin"); + argp[a++] = strdup ("-i"); + argp[a++] = strdup (_path.c_str ()); + if (_channel != ALL_CHANNELS) { + argp[a++] = strdup ("-map_channel"); + snprintf (tmp, sizeof(tmp), "0.0.%d", _channel); + argp[a++] = strdup (tmp); + } + argp[a++] = strdup ("-f"); +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + argp[a++] = strdup ("f32le"); +#else + argp[a++] = strdup ("f32be"); +#endif + argp[a++] = strdup ("-"); + + _ffmpeg_exec = new ARDOUR::SystemExec (ffmpeg_exe, argp); + PBD::info << "Decode command: { " << _ffmpeg_exec->to_s () << "}" << endmsg; + if (_ffmpeg_exec->start ()) { + PBD::error << "FFMPEGFileImportableSource: External decoder (ffmpeg) cannot be started." << endmsg; + throw std::runtime_error("Failed to start ffmpeg"); + } + + _ffmpeg_exec->ReadStdout.connect_same_thread (_ffmpeg_conn, boost::bind (&FFMPEGFileImportableSource::did_read_data, this, _1, _2)); +} + +void +FFMPEGFileImportableSource::reset () +{ + // TODO: actually signal did_read_data to unblock + g_atomic_int_set (&_ffmpeg_should_terminate, 1); + delete _ffmpeg_exec; + _ffmpeg_conn.disconnect (); + _buffer.reset (); + _read_pos = 0; + g_atomic_int_set (&_ffmpeg_should_terminate, 0); +} + +void +FFMPEGFileImportableSource::did_read_data (std::string data, size_t size) +{ + // Prepend the left-over data from a previous chunk of received data to this chunk. + data = _leftover_data + data; + samplecnt_t n_samples = data.length () / sizeof(float); + + // Stash leftover data. + _leftover_data = data.substr(n_samples * sizeof(float)); + + const char* cur = data.data (); + while (n_samples > 0) { + if (g_atomic_int_get (&_ffmpeg_should_terminate)) { + break; + } + + PBD::RingBuffer::rw_vector wv; + _buffer.get_write_vector (&wv); + if (wv.len[0] == 0) { + // TODO: don't just spin, but use some signalling + Glib::usleep (1000); + continue; + } + + samplecnt_t written = 0; + for (int i = 0; i < 2; ++i) { + samplecnt_t cnt = std::min(n_samples, wv.len[i]); + memcpy (wv.buf[i], cur, cnt * sizeof(float)); + written += cnt; + n_samples -= cnt; + cur += cnt * sizeof(float); + } + _buffer.increment_write_idx (written); + } +} + +} diff --git a/libs/ardour/ffmpegfilesource.cc b/libs/ardour/ffmpegfilesource.cc new file mode 100644 index 0000000000..7703b1cfb5 --- /dev/null +++ b/libs/ardour/ffmpegfilesource.cc @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 Marijn Kruisselbrink + * + * This program 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 program 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. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "ardour/ffmpegfileimportable.h" +#include "ardour/ffmpegfilesource.h" +#include "ardour/filesystem_paths.h" + +namespace ARDOUR { + +/** Constructor to be called for existing external-to-session files + * Sources created with this method are never writable or removable. + */ + +FFMPEGFileSource::FFMPEGFileSource (Session& s, const std::string& path, int chn, Flag flags) + : Source (s, DataType::AUDIO, path, + Source::Flag (flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy))) + , AudioFileSource (s, path, + Source::Flag (flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy))) + , _ffmpeg (path, chn) +{ + _length = _ffmpeg.length (); +} + +FFMPEGFileSource::~FFMPEGFileSource () +{ +} + +void +FFMPEGFileSource::close () +{ +} + +samplecnt_t +FFMPEGFileSource::read_unlocked (Sample* dst, samplepos_t start, samplecnt_t cnt) const +{ + _ffmpeg.seek (start); + return _ffmpeg.read (dst, cnt); +} + +int +FFMPEGFileSource::get_soundfile_info (const std::string& path, SoundFileInfo &_info, std::string &error_msg) +{ + if (!safe_audio_file_extension (path)) { + return -1; + } + + try { + FFMPEGFileImportableSource ffmpeg (path); + _info.samplerate = ffmpeg.samplerate (); + _info.channels = ffmpeg.channels (); + _info.length = ffmpeg.length (); + _info.format_name = ffmpeg.format_name (); + _info.timecode = ffmpeg.natural_position (); + _info.seekable = false; + return 0; + } catch (...) {} + return -1; +} + +bool +FFMPEGFileSource::safe_audio_file_extension (const std::string &file) +{ + std::string unused; + if (!ArdourVideoToolPaths::transcoder_exe (unused, unused)) { + return false; + } + + const char *suffixes[] = { + ".m4a", ".M4A", + }; + + for (size_t n = 0; n < sizeof(suffixes) / sizeof(suffixes[0]); ++n) { + if (file.rfind(suffixes[n]) == file.length() - strlen(suffixes[n])) { + return true; + } + } + + return false; +} + +} diff --git a/libs/ardour/import.cc b/libs/ardour/import.cc index 74f17c17e5..7852ac0dfc 100644 --- a/libs/ardour/import.cc +++ b/libs/ardour/import.cc @@ -53,6 +53,7 @@ #include "ardour/ardour.h" #include "ardour/audioengine.h" #include "ardour/audioregion.h" +#include "ardour/ffmpegfileimportable.h" #include "ardour/import_status.h" #include "ardour/mp3fileimportable.h" #include "ardour/region_factory.h" @@ -123,6 +124,18 @@ open_importable_source (const string& path, samplecnt_t samplerate, ARDOUR::SrcQ return boost::shared_ptr(new ResampledImportableSource(source, samplerate, quality)); } catch (...) { } + /* finally try FFMPEG */ + try { + boost::shared_ptr source(new FFMPEGFileImportableSource(path)); + + if (source->samplerate() == samplerate) { + return source; + } + + /* rewrap as a resampled source */ + return boost::shared_ptr(new ResampledImportableSource(source, samplerate, quality)); + } catch (...) { } + throw failed_constructor (); } diff --git a/libs/ardour/source_factory.cc b/libs/ardour/source_factory.cc index 2ceea55988..aeb1f15ffc 100644 --- a/libs/ardour/source_factory.cc +++ b/libs/ardour/source_factory.cc @@ -31,6 +31,7 @@ #include "ardour/audioplaylist.h" #include "ardour/audio_playlist_source.h" #include "ardour/boost_debug.h" +#include "ardour/ffmpegfilesource.h" #include "ardour/midi_playlist.h" #include "ardour/midi_playlist_source.h" #include "ardour/mp3filesource.h" @@ -272,6 +273,14 @@ SourceFactory::createExternal (DataType type, Session& s, const string& path, return ret; } catch (failed_constructor& err) { } + + try { + Source* src = new FFMPEGFileSource (s, path, chn, flags); + boost::shared_ptr ret (src); + BOOST_MARK_SOURCE (ret); + return ret; + + } catch (failed_constructor& err) { } } } else if (type == DataType::MIDI) { diff --git a/libs/ardour/wscript b/libs/ardour/wscript index 7dffae14dc..d24dcbe58c 100644 --- a/libs/ardour/wscript +++ b/libs/ardour/wscript @@ -92,6 +92,8 @@ libardour_sources = [ 'export_profile_manager.cc', 'export_status.cc', 'export_timespan.cc', + 'ffmpegfileimportable.cc', + 'ffmpegfilesource.cc', 'file_source.cc', 'filename_extensions.cc', 'filesystem_paths.cc',