Turn off play button on stop.
[dcpomatic.git] / src / wx / film_viewer.cc
index 8fff213454b2193c4fa8d218cf2e798cfe4a7980..942a0fedcd27717faf146d2599e625ab412948fb 100644 (file)
@@ -65,6 +65,13 @@ using boost::weak_ptr;
 using boost::optional;
 using dcp::Size;
 
+static
+int
+rtaudio_callback (void* out, void *, unsigned int frames, double, RtAudioStreamStatus, void* data)
+{
+       return reinterpret_cast<FilmViewer*>(data)->audio_callback (out, frames);
+}
+
 FilmViewer::FilmViewer (wxWindow* p)
        : wxPanel (p)
        , _panel (new wxPanel (this))
@@ -81,6 +88,11 @@ FilmViewer::FilmViewer (wxWindow* p)
        , _coalesce_player_changes (false)
        , _pending_player_change (false)
        , _last_seek_accurate (true)
+       , _audio (DCPOMATIC_RTAUDIO_API)
+       , _audio_channels (0)
+       , _audio_block_size (1024)
+       , _playing (false)
+       , _latency_history_count (0)
 {
 #ifndef __WXOSX__
        _panel->SetDoubleBuffered (true);
@@ -143,6 +155,14 @@ FilmViewer::FilmViewer (wxWindow* p)
                );
 
        setup_sensitivity ();
+
+       _config_changed_connection = Config::instance()->Changed.connect (bind (&FilmViewer::config_changed, this, _1));
+       config_changed (Config::SOUND_OUTPUT);
+}
+
+FilmViewer::~FilmViewer ()
+{
+       stop ();
 }
 
 void
@@ -176,12 +196,14 @@ FilmViewer::set_film (shared_ptr<Film> film)
           in the preview.
        */
        _player->set_always_burn_subtitles (true);
-       _player->set_ignore_audio ();
        _player->set_play_referenced ();
 
        _film->Changed.connect (boost::bind (&FilmViewer::film_changed, this, _1));
        _player->Changed.connect (boost::bind (&FilmViewer::player_changed, this, _1));
 
+       /* Keep about 1 second's worth of history samples */
+       _latency_history_count = _film->audio_frame_rate() / _audio_block_size;
+
        recreate_butler ();
 
        calculate_sizes ();
@@ -193,13 +215,34 @@ FilmViewer::set_film (shared_ptr<Film> film)
 void
 FilmViewer::recreate_butler ()
 {
+       bool const was_running = stop ();
        _butler.reset ();
 
        if (!_film) {
                return;
        }
 
-       _butler.reset (new Butler (_film, _player));
+       AudioMapping map = AudioMapping (_film->audio_channels(), _audio_channels);
+
+       if (_audio_channels != 2 || _film->audio_channels() < 3) {
+               for (int i = 0; i < min (_film->audio_channels(), _audio_channels); ++i) {
+                       map.set (i, i, 1);
+               }
+       } else {
+               /* Special case: stereo output, at least 3 channel input, map L+R to L/R and
+                  C to both, all 3dB down.
+               */
+               map.set (0, 0, 1 / sqrt(2)); // L -> L
+               map.set (1, 1, 1 / sqrt(2)); // R -> R
+               map.set (2, 0, 1 / sqrt(2)); // C -> L
+               map.set (2, 1, 1 / sqrt(2)); // C -> R
+       }
+
+       _butler.reset (new Butler (_film, _player, map, _audio_channels));
+
+       if (was_running) {
+               start ();
+       }
 }
 
 void
@@ -215,9 +258,14 @@ FilmViewer::get ()
        pair<shared_ptr<PlayerVideo>, DCPTime> video;
        do {
                video = _butler->get_video ();
-       } while (_film->three_d() && ((_left_eye->GetValue() && video.first->eyes() == EYES_RIGHT) || (_right_eye->GetValue() && video.first->eyes() == EYES_LEFT)));
+       } while (
+               _film->three_d() &&
+               ((_left_eye->GetValue() && video.first->eyes() == EYES_RIGHT) || (_right_eye->GetValue() && video.first->eyes() == EYES_LEFT))
+               );
 
        if (!video.first) {
+               _frame.reset ();
+               refresh_panel ();
                return;
        }
 
@@ -247,7 +295,7 @@ FilmViewer::get ()
 
        ImageChanged (video.first);
 
-       _position = video.second;
+       _video_position = video.second;
        _inter_position = video.first->inter_position ();
        _inter_size = video.first->inter_size ();
 
@@ -257,17 +305,26 @@ FilmViewer::get ()
 void
 FilmViewer::timer ()
 {
-       DCPTime const frame = DCPTime::from_frames (1, _film->video_frame_rate ());
+       if (!_film) {
+               return;
+       }
 
-       if ((_position + frame) >= _film->length ()) {
-               _play_button->SetValue (false);
-               check_play_state ();
-       } else {
+       if (_audio.isStreamRunning ()) {
                get ();
+               update_position_label ();
+               update_position_slider ();
+               DCPTime const next = _video_position + DCPTime::from_frames (1, _film->video_frame_rate ());
+
+               if (next >= _film->length()) {
+                       stop ();
+               }
+
+               _timer.Start (max ((next.seconds() - time().seconds()) * 1000, 0.0), wxTIMER_ONE_SHOT);
        }
 
-       update_position_label ();
-       update_position_slider ();
+       if (_butler) {
+               _butler->rethrow ();
+       }
 }
 
 void
@@ -379,10 +436,39 @@ FilmViewer::check_play_state ()
        }
 
        if (_play_button->GetValue()) {
-               _timer.Start (1000 / _film->video_frame_rate());
+               start ();
        } else {
-               _timer.Stop ();
+               stop ();
+       }
+}
+
+void
+FilmViewer::start ()
+{
+       if (_audio.isStreamOpen()) {
+               _audio.setStreamTime (_video_position.seconds());
+               _audio.startStream ();
+       }
+
+       _playing = true;
+       _timer.Start (0, wxTIMER_ONE_SHOT);
+}
+
+bool
+FilmViewer::stop ()
+{
+       if (_audio.isStreamRunning()) {
+               /* stop stream and discard any remainig queued samples */
+               _audio.abortStream ();
+       }
+
+       if (!_playing) {
+               return false;
        }
+
+       _playing = false;
+       _play_button->SetValue (false);
+       return true;
 }
 
 void
@@ -396,7 +482,7 @@ FilmViewer::update_position_slider ()
        DCPTime const len = _film->length ();
 
        if (len.get ()) {
-               int const new_slider_position = 4096 * _position.get() / len.get();
+               int const new_slider_position = 4096 * _video_position.get() / len.get();
                if (new_slider_position != _slider->GetValue()) {
                        _slider->SetValue (new_slider_position);
                }
@@ -414,8 +500,8 @@ FilmViewer::update_position_label ()
 
        double const fps = _film->video_frame_rate ();
        /* Count frame number from 1 ... not sure if this is the best idea */
-       _frame_number->SetLabel (wxString::Format (wxT("%ld"), lrint (_position.seconds() * fps) + 1));
-       _timecode->SetLabel (time_to_timecode (_position, fps));
+       _frame_number->SetLabel (wxString::Format (wxT("%ld"), lrint (_video_position.seconds() * fps) + 1));
+       _timecode->SetLabel (time_to_timecode (_video_position, fps));
 }
 
 void
@@ -462,14 +548,14 @@ FilmViewer::go_to (DCPTime t)
 void
 FilmViewer::back_clicked (wxMouseEvent& ev)
 {
-       go_to (_position - nudge_amount (ev));
+       go_to (_video_position - nudge_amount (ev));
        ev.Skip ();
 }
 
 void
 FilmViewer::forward_clicked (wxMouseEvent& ev)
 {
-       go_to (_position + nudge_amount (ev));
+       go_to (_video_position + nudge_amount (ev));
        ev.Skip ();
 }
 
@@ -503,6 +589,7 @@ FilmViewer::setup_sensitivity ()
        _outline_content->Enable (c);
        _frame_number->Enable (c);
        _timecode->Enable (c);
+       _jump_to_selected->Enable (c);
 
        _left_eye->Enable (c && _film->three_d ());
        _right_eye->Enable (c && _film->three_d ());
@@ -520,13 +607,13 @@ FilmViewer::film_changed (Film::Property p)
 void
 FilmViewer::refresh ()
 {
-       seek (_position, _last_seek_accurate);
+       seek (_video_position, _last_seek_accurate);
 }
 
 void
 FilmViewer::set_position (DCPTime p)
 {
-       _position = p;
+       _video_position = p;
        seek (p, true);
        update_position_label ();
        update_position_slider ();
@@ -583,3 +670,90 @@ FilmViewer::seek (DCPTime t, bool accurate)
        _last_seek_accurate = accurate;
        get ();
 }
+
+void
+FilmViewer::config_changed (Config::Property p)
+{
+       if (p != Config::SOUND_OUTPUT) {
+               return;
+       }
+
+       if (_audio.isStreamOpen ()) {
+               _audio.closeStream ();
+       }
+
+       unsigned int st = 0;
+       if (Config::instance()->sound_output()) {
+               while (st < _audio.getDeviceCount()) {
+                       if (_audio.getDeviceInfo(st).name == Config::instance()->sound_output().get()) {
+                               break;
+                       }
+                       ++st;
+               }
+               if (st == _audio.getDeviceCount()) {
+                       st = _audio.getDefaultOutputDevice();
+               }
+       } else {
+               st = _audio.getDefaultOutputDevice();
+       }
+
+       _audio_channels = _audio.getDeviceInfo(st).outputChannels;
+       recreate_butler ();
+
+       RtAudio::StreamParameters sp;
+       sp.deviceId = st;
+       sp.nChannels = _audio_channels;
+       sp.firstChannel = 0;
+       try {
+               _audio.openStream (&sp, 0, RTAUDIO_FLOAT32, 48000, &_audio_block_size, &rtaudio_callback, this);
+#ifdef DCPOMATIC_USE_RTERROR
+       } catch (RtError& e) {
+#else
+       } catch (RtAudioError& e) {
+#endif
+               error_dialog (this, wxString::Format (_("Could not set up audio output (%s).  There will be no audio during the preview."), e.what()));
+       }
+}
+
+DCPTime
+FilmViewer::time () const
+{
+       if (_audio.isStreamRunning ()) {
+               return DCPTime::from_seconds (const_cast<RtAudio*>(&_audio)->getStreamTime ()) -
+                       DCPTime::from_frames (average_latency(), _film->audio_frame_rate());
+       }
+
+       return _video_position;
+}
+
+int
+FilmViewer::audio_callback (void* out_p, unsigned int frames)
+{
+       _butler->get_audio (reinterpret_cast<float*> (out_p), frames);
+
+        boost::mutex::scoped_lock lm (_latency_history_mutex, boost::try_to_lock);
+        if (lm) {
+                _latency_history.push_back (_audio.getStreamLatency ());
+                if (_latency_history.size() > static_cast<size_t> (_latency_history_count)) {
+                        _latency_history.pop_front ();
+                }
+        }
+
+       return 0;
+}
+
+Frame
+FilmViewer::average_latency () const
+{
+        boost::mutex::scoped_lock lm (_latency_history_mutex);
+        if (_latency_history.empty()) {
+                return 0;
+        }
+
+        Frame total = 0;
+        BOOST_FOREACH (Frame i, _latency_history) {
+                total += i;
+        }
+
+        return total / _latency_history.size();
+}