2 Copyright (C) 2013-2016 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
23 #include "audio_buffers.h"
24 #include "content_audio.h"
25 #include "dcp_content.h"
28 #include "raw_image_proxy.h"
31 #include "render_subtitles.h"
33 #include "content_video.h"
34 #include "player_video.h"
35 #include "frame_rate_change.h"
36 #include "audio_processor.h"
38 #include "referenced_reel_asset.h"
39 #include "decoder_factory.h"
41 #include "video_decoder.h"
42 #include "audio_decoder.h"
43 #include "subtitle_content.h"
44 #include "subtitle_decoder.h"
45 #include "ffmpeg_content.h"
46 #include "audio_content.h"
47 #include "content_subtitle.h"
48 #include "dcp_decoder.h"
49 #include "image_decoder.h"
50 #include "resampler.h"
51 #include "compose.hpp"
53 #include <dcp/reel_sound_asset.h>
54 #include <dcp/reel_subtitle_asset.h>
55 #include <dcp/reel_picture_asset.h>
56 #include <boost/foreach.hpp>
63 #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL);
75 using boost::shared_ptr;
76 using boost::weak_ptr;
77 using boost::dynamic_pointer_cast;
78 using boost::optional;
79 using boost::scoped_ptr;
81 Player::Player (shared_ptr<const Film> film, shared_ptr<const Playlist> playlist)
83 , _playlist (playlist)
84 , _have_valid_pieces (false)
85 , _ignore_video (false)
86 , _ignore_audio (false)
87 , _always_burn_subtitles (false)
89 , _play_referenced (false)
90 , _audio_merger (_film->audio_channels(), _film->audio_frame_rate())
92 _film_changed_connection = _film->Changed.connect (bind (&Player::film_changed, this, _1));
93 _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this));
94 _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::playlist_content_changed, this, _1, _2, _3));
95 set_video_container_size (_film->frame_size ());
97 film_changed (Film::AUDIO_PROCESSOR);
101 Player::setup_pieces ()
105 BOOST_FOREACH (shared_ptr<Content> i, _playlist->content ()) {
107 if (!i->paths_valid ()) {
111 shared_ptr<Decoder> decoder = decoder_factory (i, _film->log());
112 FrameRateChange frc (i->active_video_frame_rate(), _film->video_frame_rate());
115 /* Not something that we can decode; e.g. Atmos content */
119 if (decoder->video && _ignore_video) {
120 decoder->video->set_ignore ();
123 if (decoder->audio && _ignore_audio) {
124 decoder->audio->set_ignore ();
127 shared_ptr<DCPDecoder> dcp = dynamic_pointer_cast<DCPDecoder> (decoder);
128 if (dcp && _play_referenced) {
129 dcp->set_decode_referenced ();
132 shared_ptr<Piece> piece (new Piece (i, decoder, frc));
133 _pieces.push_back (piece);
135 if (decoder->video) {
136 decoder->video->Data.connect (bind (&Player::video, this, weak_ptr<Piece> (piece), _1));
139 if (decoder->audio) {
140 decoder->audio->Data.connect (bind (&Player::audio, this, weak_ptr<Piece> (piece), _1, _2));
143 if (decoder->subtitle) {
144 decoder->subtitle->ImageData.connect (bind (&Player::image_subtitle, this, weak_ptr<Piece> (piece), _1));
145 decoder->subtitle->TextData.connect (bind (&Player::text_subtitle, this, weak_ptr<Piece> (piece), _1));
149 _have_valid_pieces = true;
153 Player::playlist_content_changed (weak_ptr<Content> w, int property, bool frequent)
155 shared_ptr<Content> c = w.lock ();
161 property == ContentProperty::POSITION ||
162 property == ContentProperty::LENGTH ||
163 property == ContentProperty::TRIM_START ||
164 property == ContentProperty::TRIM_END ||
165 property == ContentProperty::PATH ||
166 property == VideoContentProperty::FRAME_TYPE ||
167 property == DCPContentProperty::NEEDS_ASSETS ||
168 property == DCPContentProperty::NEEDS_KDM ||
169 property == SubtitleContentProperty::COLOUR ||
170 property == SubtitleContentProperty::OUTLINE ||
171 property == SubtitleContentProperty::SHADOW ||
172 property == SubtitleContentProperty::EFFECT_COLOUR ||
173 property == FFmpegContentProperty::SUBTITLE_STREAM ||
174 property == VideoContentProperty::COLOUR_CONVERSION
177 _have_valid_pieces = false;
181 property == SubtitleContentProperty::LINE_SPACING ||
182 property == SubtitleContentProperty::OUTLINE_WIDTH ||
183 property == SubtitleContentProperty::Y_SCALE ||
184 property == SubtitleContentProperty::FADE_IN ||
185 property == SubtitleContentProperty::FADE_OUT ||
186 property == ContentProperty::VIDEO_FRAME_RATE ||
187 property == SubtitleContentProperty::USE ||
188 property == SubtitleContentProperty::X_OFFSET ||
189 property == SubtitleContentProperty::Y_OFFSET ||
190 property == SubtitleContentProperty::X_SCALE ||
191 property == SubtitleContentProperty::FONTS ||
192 property == VideoContentProperty::CROP ||
193 property == VideoContentProperty::SCALE ||
194 property == VideoContentProperty::FADE_IN ||
195 property == VideoContentProperty::FADE_OUT
203 Player::set_video_container_size (dcp::Size s)
205 _video_container_size = s;
207 _black_image.reset (new Image (AV_PIX_FMT_RGB24, _video_container_size, true));
208 _black_image->make_black ();
212 Player::playlist_changed ()
214 _have_valid_pieces = false;
219 Player::film_changed (Film::Property p)
221 /* Here we should notice Film properties that affect our output, and
222 alert listeners that our output now would be different to how it was
223 last time we were run.
226 if (p == Film::CONTAINER) {
228 } else if (p == Film::VIDEO_FRAME_RATE) {
229 /* Pieces contain a FrameRateChange which contains the DCP frame rate,
230 so we need new pieces here.
232 _have_valid_pieces = false;
234 } else if (p == Film::AUDIO_PROCESSOR) {
235 if (_film->audio_processor ()) {
236 _audio_processor = _film->audio_processor()->clone (_film->audio_frame_rate ());
242 Player::transform_image_subtitles (list<ImageSubtitle> subs) const
244 list<PositionImage> all;
246 for (list<ImageSubtitle>::const_iterator i = subs.begin(); i != subs.end(); ++i) {
251 /* We will scale the subtitle up to fit _video_container_size */
252 dcp::Size scaled_size (i->rectangle.width * _video_container_size.width, i->rectangle.height * _video_container_size.height);
254 /* Then we need a corrective translation, consisting of two parts:
256 * 1. that which is the result of the scaling of the subtitle by _video_container_size; this will be
257 * rect.x * _video_container_size.width and rect.y * _video_container_size.height.
259 * 2. that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be
260 * (width_before_subtitle_scale * (1 - subtitle_x_scale) / 2) and
261 * (height_before_subtitle_scale * (1 - subtitle_y_scale) / 2).
263 * Combining these two translations gives these expressions.
270 dcp::YUV_TO_RGB_REC601,
271 i->image->pixel_format (),
276 lrint (_video_container_size.width * i->rectangle.x),
277 lrint (_video_container_size.height * i->rectangle.y)
286 shared_ptr<PlayerVideo>
287 Player::black_player_video_frame () const
289 return shared_ptr<PlayerVideo> (
291 shared_ptr<const ImageProxy> (new RawImageProxy (_black_image)),
294 _video_container_size,
295 _video_container_size,
298 PresetColourConversion::all().front().conversion
304 Player::dcp_to_content_video (shared_ptr<const Piece> piece, DCPTime t) const
306 DCPTime s = t - piece->content->position ();
307 s = min (piece->content->length_after_trim(), s);
308 s = max (DCPTime(), s + DCPTime (piece->content->trim_start(), piece->frc));
310 /* It might seem more logical here to convert s to a ContentTime (using the FrameRateChange)
311 then convert that ContentTime to frames at the content's rate. However this fails for
312 situations like content at 29.9978733fps, DCP at 30fps. The accuracy of the Time type is not
313 enough to distinguish between the two with low values of time (e.g. 3200 in Time units).
315 Instead we convert the DCPTime using the DCP video rate then account for any skip/repeat.
317 return s.frames_floor (piece->frc.dcp) / piece->frc.factor ();
321 Player::content_video_to_dcp (shared_ptr<const Piece> piece, Frame f) const
323 /* See comment in dcp_to_content_video */
324 DCPTime const d = DCPTime::from_frames (f * piece->frc.factor(), piece->frc.dcp) - DCPTime (piece->content->trim_start (), piece->frc);
325 return max (DCPTime (), d + piece->content->position ());
329 Player::dcp_to_resampled_audio (shared_ptr<const Piece> piece, DCPTime t) const
331 DCPTime s = t - piece->content->position ();
332 s = min (piece->content->length_after_trim(), s);
333 /* See notes in dcp_to_content_video */
334 return max (DCPTime (), DCPTime (piece->content->trim_start (), piece->frc) + s).frames_floor (_film->audio_frame_rate ());
338 Player::resampled_audio_to_dcp (shared_ptr<const Piece> piece, Frame f) const
340 /* See comment in dcp_to_content_video */
341 DCPTime const d = DCPTime::from_frames (f, _film->audio_frame_rate()) - DCPTime (piece->content->trim_start (), piece->frc);
342 return max (DCPTime (), d + piece->content->position ());
346 Player::dcp_to_content_time (shared_ptr<const Piece> piece, DCPTime t) const
348 DCPTime s = t - piece->content->position ();
349 s = min (piece->content->length_after_trim(), s);
350 return max (ContentTime (), ContentTime (s, piece->frc) + piece->content->trim_start());
354 Player::content_time_to_dcp (shared_ptr<const Piece> piece, ContentTime t) const
356 return max (DCPTime (), DCPTime (t - piece->content->trim_start(), piece->frc) + piece->content->position());
359 list<shared_ptr<Font> >
360 Player::get_subtitle_fonts ()
362 if (!_have_valid_pieces) {
366 list<shared_ptr<Font> > fonts;
367 BOOST_FOREACH (shared_ptr<Piece>& p, _pieces) {
368 if (p->content->subtitle) {
369 /* XXX: things may go wrong if there are duplicate font IDs
370 with different font files.
372 list<shared_ptr<Font> > f = p->content->subtitle->fonts ();
373 copy (f.begin(), f.end(), back_inserter (fonts));
380 /** Set this player never to produce any video data */
382 Player::set_ignore_video ()
384 _ignore_video = true;
387 /** Set this player never to produce any audio data */
389 Player::set_ignore_audio ()
391 _ignore_audio = true;
394 /** Set whether or not this player should always burn text subtitles into the image,
395 * regardless of the content settings.
396 * @param burn true to always burn subtitles, false to obey content settings.
399 Player::set_always_burn_subtitles (bool burn)
401 _always_burn_subtitles = burn;
408 _have_valid_pieces = false;
412 Player::set_play_referenced ()
414 _play_referenced = true;
415 _have_valid_pieces = false;
418 list<ReferencedReelAsset>
419 Player::get_reel_assets ()
421 list<ReferencedReelAsset> a;
423 BOOST_FOREACH (shared_ptr<Content> i, _playlist->content ()) {
424 shared_ptr<DCPContent> j = dynamic_pointer_cast<DCPContent> (i);
429 scoped_ptr<DCPDecoder> decoder;
431 decoder.reset (new DCPDecoder (j, _film->log()));
437 BOOST_FOREACH (shared_ptr<dcp::Reel> k, decoder->reels()) {
439 DCPOMATIC_ASSERT (j->video_frame_rate ());
440 double const cfr = j->video_frame_rate().get();
441 Frame const trim_start = j->trim_start().frames_round (cfr);
442 Frame const trim_end = j->trim_end().frames_round (cfr);
443 int const ffr = _film->video_frame_rate ();
445 DCPTime const from = i->position() + DCPTime::from_frames (offset, _film->video_frame_rate());
446 if (j->reference_video ()) {
447 shared_ptr<dcp::ReelAsset> ra = k->main_picture ();
448 DCPOMATIC_ASSERT (ra);
449 ra->set_entry_point (ra->entry_point() + trim_start);
450 ra->set_duration (ra->duration() - trim_start - trim_end);
452 ReferencedReelAsset (ra, DCPTimePeriod (from, from + DCPTime::from_frames (ra->duration(), ffr)))
456 if (j->reference_audio ()) {
457 shared_ptr<dcp::ReelAsset> ra = k->main_sound ();
458 DCPOMATIC_ASSERT (ra);
459 ra->set_entry_point (ra->entry_point() + trim_start);
460 ra->set_duration (ra->duration() - trim_start - trim_end);
462 ReferencedReelAsset (ra, DCPTimePeriod (from, from + DCPTime::from_frames (ra->duration(), ffr)))
466 if (j->reference_subtitle ()) {
467 shared_ptr<dcp::ReelAsset> ra = k->main_subtitle ();
468 DCPOMATIC_ASSERT (ra);
469 ra->set_entry_point (ra->entry_point() + trim_start);
470 ra->set_duration (ra->duration() - trim_start - trim_end);
472 ReferencedReelAsset (ra, DCPTimePeriod (from, from + DCPTime::from_frames (ra->duration(), ffr)))
476 /* Assume that main picture duration is the length of the reel */
477 offset += k->main_picture()->duration ();
484 list<shared_ptr<Piece> >
485 Player::overlaps (DCPTime from, DCPTime to, boost::function<bool (Content *)> valid)
487 if (!_have_valid_pieces) {
491 list<shared_ptr<Piece> > overlaps;
492 BOOST_FOREACH (shared_ptr<Piece> i, _pieces) {
493 if (valid (i->content.get ()) && i->content->position() < to && i->content->end() > from) {
494 overlaps.push_back (i);
504 if (!_have_valid_pieces) {
508 shared_ptr<Piece> earliest;
509 DCPTime earliest_content;
511 BOOST_FOREACH (shared_ptr<Piece> i, _pieces) {
513 DCPTime const t = i->content->position() + DCPTime (i->decoder->position(), i->frc);
514 if (!earliest || t < earliest_content) {
515 earliest_content = t;
525 earliest->done = earliest->decoder->pass ();
527 /* Emit any audio that is ready */
529 optional<DCPTime> earliest_audio;
530 BOOST_FOREACH (shared_ptr<Piece> i, _pieces) {
531 if (i->decoder->audio) {
532 DCPTime const t = i->content->position() + DCPTime (i->decoder->audio->position(), i->frc);
533 if (!earliest_audio || t < *earliest_audio) {
539 pair<shared_ptr<AudioBuffers>, DCPTime> audio = _audio_merger.pull (earliest_audio.get_value_or(DCPTime()));
540 if (audio.first->frames() > 0) {
541 DCPOMATIC_ASSERT (audio.second >= _last_audio_time);
542 DCPTime t = _last_audio_time;
543 while (t < audio.second) {
544 /* Silence up to the time of this new audio */
545 DCPTime block = min (DCPTime::from_seconds (0.5), audio.second - t);
546 shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), block.frames_round(_film->audio_frame_rate())));
547 silence->make_silent ();
552 Audio (audio.first, audio.second);
553 _last_audio_time = audio.second;
560 Player::video (weak_ptr<Piece> wp, ContentVideo video)
562 shared_ptr<Piece> piece = wp.lock ();
567 /* Time and period of the frame we will emit */
568 DCPTime const time = content_video_to_dcp (piece, video.frame.index());
569 DCPTimePeriod const period (time, time + DCPTime::from_frames (1, _film->video_frame_rate()));
571 /* Discard if it's outside the content's period */
572 if (time < piece->content->position() || time >= piece->content->end()) {
576 /* Get any subtitles */
578 optional<PositionImage> subtitles;
580 for (list<pair<PlayerSubtitles, DCPTimePeriod> >::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
582 if (!i->second.overlap (period)) {
586 list<PositionImage> sub_images;
588 /* Image subtitles */
589 list<PositionImage> c = transform_image_subtitles (i->first.image);
590 copy (c.begin(), c.end(), back_inserter (sub_images));
592 /* Text subtitles (rendered to an image) */
593 if (!i->first.text.empty ()) {
594 list<PositionImage> s = render_subtitles (i->first.text, i->first.fonts, _video_container_size, time);
595 copy (s.begin (), s.end (), back_inserter (sub_images));
598 if (!sub_images.empty ()) {
599 subtitles = merge (sub_images);
606 /* XXX: this may not work for 3D */
607 DCPTime const frame = DCPTime::from_frames (1, _film->video_frame_rate());
608 for (DCPTime i = _last_time.get() + frame; i < time; i += frame) {
609 if (_playlist->video_content_at(i) && _last_video) {
610 Video (shared_ptr<PlayerVideo> (new PlayerVideo (*_last_video)), i);
612 Video (black_player_video_frame (), i);
620 piece->content->video->crop (),
621 piece->content->video->fade (video.frame.index()),
622 piece->content->video->scale().size (
623 piece->content->video, _video_container_size, _film->frame_size ()
625 _video_container_size,
628 piece->content->video->colour_conversion ()
633 _last_video->set_subtitle (subtitles.get ());
638 Video (_last_video, *_last_time);
640 /* Discard any subtitles we no longer need */
642 for (list<pair<PlayerSubtitles, DCPTimePeriod> >::iterator i = _subtitles.begin (); i != _subtitles.end(); ) {
643 list<pair<PlayerSubtitles, DCPTimePeriod> >::iterator tmp = i;
646 if (i->second.to < time) {
647 _subtitles.erase (i);
655 Player::audio (weak_ptr<Piece> wp, AudioStreamPtr stream, ContentAudio content_audio)
657 shared_ptr<Piece> piece = wp.lock ();
662 shared_ptr<AudioContent> content = piece->content->audio;
663 DCPOMATIC_ASSERT (content);
666 if (content->gain() != 0) {
667 shared_ptr<AudioBuffers> gain (new AudioBuffers (content_audio.audio));
668 gain->apply_gain (content->gain ());
669 content_audio.audio = gain;
673 if (stream->frame_rate() != content->resampled_frame_rate()) {
674 shared_ptr<Resampler> r = resampler (content, stream, true);
675 pair<shared_ptr<const AudioBuffers>, Frame> ro = r->run (content_audio.audio, content_audio.frame);
676 content_audio.audio = ro.first;
677 content_audio.frame = ro.second;
680 /* XXX: end-trimming used to be checked here */
682 /* Compute time in the DCP */
683 DCPTime time = resampled_audio_to_dcp (piece, content_audio.frame) + DCPTime::from_seconds (content->delay() / 1000);
686 shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), content_audio.audio->frames()));
687 dcp_mapped->make_silent ();
689 AudioMapping map = stream->mapping ();
690 for (int i = 0; i < map.input_channels(); ++i) {
691 for (int j = 0; j < dcp_mapped->channels(); ++j) {
692 if (map.get (i, static_cast<dcp::Channel> (j)) > 0) {
693 dcp_mapped->accumulate_channel (
694 content_audio.audio.get(),
696 static_cast<dcp::Channel> (j),
697 map.get (i, static_cast<dcp::Channel> (j))
703 content_audio.audio = dcp_mapped;
705 if (_audio_processor) {
706 content_audio.audio = _audio_processor->run (content_audio.audio, _film->audio_channels ());
709 /* XXX: this may be nonsense */
710 if (time < _audio_merger.last_pull()) {
711 DCPTime const discard_time = _audio_merger.last_pull() - time;
712 Frame discard_frames = discard_time.frames_round(_film->audio_frame_rate());
713 content_audio.audio.reset (new AudioBuffers (_film->audio_channels(), content_audio.audio->frames() - discard_frames));
714 time += discard_time;
717 if (content_audio.audio->frames() > 0) {
718 _audio_merger.push (content_audio.audio, time);
723 Player::image_subtitle (weak_ptr<Piece> wp, ContentImageSubtitle subtitle)
725 shared_ptr<Piece> piece = wp.lock ();
730 /* Apply content's subtitle offsets */
731 subtitle.sub.rectangle.x += piece->content->subtitle->x_offset ();
732 subtitle.sub.rectangle.y += piece->content->subtitle->y_offset ();
734 /* Apply content's subtitle scale */
735 subtitle.sub.rectangle.width *= piece->content->subtitle->x_scale ();
736 subtitle.sub.rectangle.height *= piece->content->subtitle->y_scale ();
738 /* Apply a corrective translation to keep the subtitle centred after that scale */
739 subtitle.sub.rectangle.x -= subtitle.sub.rectangle.width * (piece->content->subtitle->x_scale() - 1);
740 subtitle.sub.rectangle.y -= subtitle.sub.rectangle.height * (piece->content->subtitle->y_scale() - 1);
743 ps.image.push_back (subtitle.sub);
744 DCPTimePeriod period (content_time_to_dcp (piece, subtitle.period().from), content_time_to_dcp (piece, subtitle.period().to));
746 if (piece->content->subtitle->use() && (piece->content->subtitle->burn() || _always_burn_subtitles)) {
747 _subtitles.push_back (make_pair (ps, period));
749 Subtitle (ps, period);
754 Player::text_subtitle (weak_ptr<Piece> wp, ContentTextSubtitle subtitle)
756 shared_ptr<Piece> piece = wp.lock ();
762 DCPTimePeriod const period (content_time_to_dcp (piece, subtitle.period().from), content_time_to_dcp (piece, subtitle.period().to));
764 BOOST_FOREACH (dcp::SubtitleString s, subtitle.subs) {
765 s.set_h_position (s.h_position() + piece->content->subtitle->x_offset ());
766 s.set_v_position (s.v_position() + piece->content->subtitle->y_offset ());
767 float const xs = piece->content->subtitle->x_scale();
768 float const ys = piece->content->subtitle->y_scale();
769 float size = s.size();
771 /* Adjust size to express the common part of the scaling;
772 e.g. if xs = ys = 0.5 we scale size by 2.
774 if (xs > 1e-5 && ys > 1e-5) {
775 size *= 1 / min (1 / xs, 1 / ys);
779 /* Then express aspect ratio changes */
780 if (fabs (1.0 - xs / ys) > dcp::ASPECT_ADJUST_EPSILON) {
781 s.set_aspect_adjust (xs / ys);
784 s.set_in (dcp::Time(period.from.seconds(), 1000));
785 s.set_out (dcp::Time(period.to.seconds(), 1000));
786 ps.text.push_back (SubtitleString (s, piece->content->subtitle->outline_width()));
787 ps.add_fonts (piece->content->subtitle->fonts ());
790 if (piece->content->subtitle->use() && (piece->content->subtitle->burn() || _always_burn_subtitles)) {
791 _subtitles.push_back (make_pair (ps, period));
793 Subtitle (ps, period);
798 Player::seek (DCPTime time, bool accurate)
800 BOOST_FOREACH (shared_ptr<Piece> i, _pieces) {
801 if (i->content->position() <= time && time < i->content->end()) {
802 i->decoder->seek (dcp_to_content_time (i, time), accurate);
808 _last_time = time - DCPTime::from_frames (1, _film->video_frame_rate ());
810 _last_time = optional<DCPTime> ();
814 shared_ptr<Resampler>
815 Player::resampler (shared_ptr<const AudioContent> content, AudioStreamPtr stream, bool create)
817 ResamplerMap::const_iterator i = _resamplers.find (make_pair (content, stream));
818 if (i != _resamplers.end ()) {
823 return shared_ptr<Resampler> ();
827 "Creating new resampler from %1 to %2 with %3 channels",
828 stream->frame_rate(),
829 content->resampled_frame_rate(),
833 shared_ptr<Resampler> r (
834 new Resampler (stream->frame_rate(), content->resampled_frame_rate(), stream->channels())
837 _resamplers[make_pair(content, stream)] = r;