1 /*
2  * Copyright (C) 2021 Robin Gareus <robin@gareus.org>
3  * Copyright (C) 2021 Ben Loftis <ben@harrisonconsoles.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19 
20 #ifdef WAF_BUILD
21 #include "gtk2ardour-config.h"
22 #endif
23 
24 #include <cassert>
25 
26 #include "ardour/session.h"
27 #include "ardour/session_route.h"
28 #include "ardour/track.h"
29 
30 #include "gtkmm2ext/utils.h"
31 #include "temporal/time.h"
32 
33 #include "audio_clock.h"
34 #include "gui_thread.h"
35 #include "rec_info_box.h"
36 #include "timers.h"
37 #include "ui_config.h"
38 
39 #include "pbd/i18n.h"
40 
41 using namespace ARDOUR;
42 
RecInfoBox()43 RecInfoBox::RecInfoBox ()
44 {
45 	set_name (X_("RecInfoBox"));
46 	_layout_label = Pango::Layout::create (get_pango_context ());
47 	_layout_value = Pango::Layout::create (get_pango_context ());
48 
49 	UIConfiguration::instance().DPIReset.connect (sigc::mem_fun (*this, &RecInfoBox::dpi_reset));
50 	dpi_reset ();
51 }
52 
53 void
on_size_request(Gtk::Requisition * r)54 RecInfoBox::on_size_request (Gtk::Requisition* r)
55 {
56 	r->width  = _width;
57 	r->height = std::max (12, _height);
58 }
59 
60 void
on_size_allocate(Gtk::Allocation & a)61 RecInfoBox::on_size_allocate (Gtk::Allocation& a)
62 {
63   CairoWidget::on_size_allocate (a);
64 }
65 
66 void
set_session(Session * s)67 RecInfoBox::set_session (Session* s)
68 {
69 	SessionHandlePtr::set_session (s);
70 
71 	if (_session) {
72 		update ();
73 	}
74 }
75 
76 void
update()77 RecInfoBox::update ()
78 {
79 	set_dirty ();
80 }
81 
82 /* ****************************************************************************/
83 
84 void
set_session(Session * s)85 DurationInfoBox::set_session (Session* s)
86 {
87 	RecInfoBox::set_session (s);
88 
89 	if (!_session) {
90 		_rectime_connection.disconnect ();
91 		return;
92 	}
93 	_session->RecordStateChanged.connect (_session_connections, invalidator (*this), boost::bind (&DurationInfoBox::rec_state_changed, this), gui_context());
94 	_session->UpdateRouteRecordState.connect (_session_connections, invalidator (*this), boost::bind (&DurationInfoBox::update, this), gui_context());
95 }
96 
97 void
rec_state_changed()98 DurationInfoBox::rec_state_changed ()
99 {
100 	if (_session && _session->actively_recording ()) {
101 		if (!_rectime_connection.connected ()) {
102 			_rectime_connection = Timers::rapid_connect (sigc::mem_fun (*this, &DurationInfoBox::update));
103 		}
104 	} else {
105 		_rectime_connection.disconnect ();
106 	}
107 	update ();
108 }
109 
110 void
dpi_reset()111 DurationInfoBox::dpi_reset ()
112 {
113 	int wv, hv;
114 	_layout_value->set_font_description (UIConfiguration::instance ().get_NormalMonospaceFont ());
115 	_layout_value->set_text ("<00:00:00:0>");
116 	_layout_value->get_pixel_size (wv, hv);
117 	_width  = 8 + wv;
118 	_height = 4 + hv;
119 	queue_resize ();
120 }
121 
122 void
render(Cairo::RefPtr<Cairo::Context> const & cr,cairo_rectangle_t * r)123 DurationInfoBox::render (Cairo::RefPtr<Cairo::Context> const& cr, cairo_rectangle_t* r)
124 {
125 	int ww = get_width ();
126 	int hh = get_height ();
127 
128 	cr->rectangle (r->x, r->y, r->width, r->height);
129 	cr->clip ();
130 	cr->set_operator (Cairo::OPERATOR_OVER);
131 
132 	bool recording;
133 	if (_session && _session->actively_recording ()) {
134 		Gtkmm2ext::set_source_rgb_a (cr, UIConfiguration::instance ().color ("alert:red"), .7);
135 		recording = true;
136 	} else {
137 		Gtkmm2ext::set_source_rgb_a (cr, UIConfiguration::instance ().color ("widget:bg"), .7);
138 		recording = false;
139 	}
140 
141 	Gtkmm2ext::rounded_rectangle (cr, 1 , 1, ww - 1, hh - 1, /*_height / 4.0 */ 4);
142 	cr->fill ();
143 
144 	if (!_session) {
145 		return;
146 	}
147 
148 	Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("neutral:foreground"));
149 
150 	samplecnt_t  capture_duration = _session->capture_duration ();
151 	samplecnt_t  sample_rate      = _session->nominal_sample_rate ();
152 
153 	int w, h;
154 
155 	if (capture_duration > 0) {
156 		char buf[32];
157 		AudioClock::print_minsec (capture_duration, buf, sizeof (buf), sample_rate, 1);
158 		if (recording) {
159 			_layout_value->set_text (string_compose(" %1 ", std::string(buf).substr(1)));
160 		} else {
161 			_layout_value->set_text (string_compose("<%1>", std::string(buf).substr(1)));
162 		}
163 	} else {
164 		_layout_value->set_text (" --:--:--:- ");
165 	}
166 	_layout_value->get_pixel_size (w, h);
167 	cr->move_to (.5 * (ww - w), hh/2 - h/2);
168 	_layout_value->show_in_cairo_context (cr);
169 }
170 
171 void
update()172 DurationInfoBox::update ()
173 {
174 	RecInfoBox::update ();
175 }
176 
177 /* ****************************************************************************/
178 
179 void
set_session(Session * s)180 XrunInfoBox::set_session (Session* s)
181 {
182 	RecInfoBox::set_session (s);
183 
184 	if (!_session) {
185 		return;
186 	}
187 
188 	_session->Xrun.connect (_session_connections, invalidator (*this), boost::bind (&XrunInfoBox::update, this), gui_context());
189 	_session->RecordStateChanged.connect (_session_connections, invalidator (*this), boost::bind (&XrunInfoBox::update, this), gui_context());
190 }
191 
192 void
dpi_reset()193 XrunInfoBox::dpi_reset ()
194 {
195 	int wv, hv;
196 	_layout_value->set_font_description (UIConfiguration::instance ().get_NormalFont ());
197 	_layout_value->set_text ("<99+>");
198 	_layout_value->get_pixel_size (wv, hv);
199 	_width  = 8 + wv;
200 	_height = 8 + hv;
201 	queue_resize ();
202 }
203 
204 void
render(Cairo::RefPtr<Cairo::Context> const & cr,cairo_rectangle_t * r)205 XrunInfoBox::render (Cairo::RefPtr<Cairo::Context> const& cr, cairo_rectangle_t* r)
206 {
207 	if (!_session) {
208 		return;
209 	}
210 
211 	int ww = get_width ();
212 	int hh = get_height ();
213 
214 	cr->rectangle (r->x, r->y, r->width, r->height);
215 	cr->clip ();
216 	cr->set_operator (Cairo::OPERATOR_OVER);
217 
218 	unsigned int xruns = _session->capture_xruns ();
219 
220 	if (xruns > 0) {
221 		Gtkmm2ext::set_source_rgb_a (cr, UIConfiguration::instance ().color ("alert:red"), .7);
222 	} else {
223 		Gtkmm2ext::set_source_rgb_a (cr, UIConfiguration::instance ().color ("widget:bg"), .7);
224 	}
225 
226 	Gtkmm2ext::rounded_rectangle (cr, 1 , 1, ww - 2, hh - 2, /*_height / 4.0 */ 4);
227 	cr->fill ();
228 
229 	if (xruns < 99) {
230 		if (_session->actively_recording ()) {
231 			_layout_value->set_text (string_compose ("%1", xruns));
232 		} else if (_session->capture_duration () > 0) {
233 			_layout_value->set_text (string_compose ("<%1>", xruns));
234 		} else {
235 			_layout_value->set_text ("-");
236 		}
237 	} else {
238 		if (_session->actively_recording ()) {
239 			_layout_value->set_text ("99+");
240 		} else if (_session->capture_duration () > 0) {
241 			_layout_value->set_text ("<99+>");
242 		} else {
243 			assert (0);
244 			return;
245 		}
246 	}
247 
248 	Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("neutral:foreground"));
249 	int w, h;
250 	_layout_value->get_pixel_size (w, h);
251 	cr->move_to (.5 * (ww - w), .5 * (hh - h));
252 	_layout_value->show_in_cairo_context (cr);
253 }
254 
255 void
update()256 XrunInfoBox::update ()
257 {
258 	RecInfoBox::update ();
259 }
260 
261 /* ****************************************************************************/
262 
263 void
set_session(Session * s)264 RemainInfoBox::set_session (Session* s)
265 {
266 	RecInfoBox::set_session (s);
267 
268 	if (!_session) {
269 		_diskspace_connection.disconnect ();
270 		return;
271 	}
272 
273 	_diskspace_connection = Timers::second_connect (sigc::mem_fun (*this, &RemainInfoBox::update));
274 	_session->UpdateRouteRecordState.connect (_session_connections, invalidator (*this), boost::bind (&RemainInfoBox::update, this), gui_context());
275 }
276 
277 void
dpi_reset()278 RemainInfoBox::dpi_reset ()
279 {
280 	_layout_label->set_font_description (UIConfiguration::instance ().get_NormalFont ());
281 	_layout_value->set_font_description (UIConfiguration::instance ().get_NormalMonospaceFont ());
282 
283 	int wl, hl, wv, hv;
284 
285 	_layout_label->set_text (_("Disk Space:"));
286 	_layout_label->get_pixel_size (wl, hl);
287 
288 	_layout_value->set_text (_(">24h"));
289 	_layout_value->get_pixel_size (wv, hv);
290 
291 	_width  = 8 + std::max (wl, wv);
292 	_height = 2 + hv + 2 + hl + 2;
293 
294 	queue_resize ();
295 }
296 
297 void
count_recenabled_streams(Route & route)298 RemainInfoBox::count_recenabled_streams (Route& route)
299 {
300   Track* track = dynamic_cast<Track*>(&route);
301   if (track && track->rec_enable_control()->get_value()) {
302     _rec_enabled_streams += track->n_inputs().n_total();
303   }
304 }
305 
306 void
render(Cairo::RefPtr<Cairo::Context> const & cr,cairo_rectangle_t * r)307 RemainInfoBox::render (Cairo::RefPtr<Cairo::Context> const& cr, cairo_rectangle_t* r)
308 {
309 	int ww = get_width ();
310 	int hh = get_height ();
311 
312 	cr->rectangle (r->x, r->y, r->width, r->height);
313 	cr->clip ();
314 	cr->set_operator (Cairo::OPERATOR_OVER);
315 
316 	if (!_session) {
317 		return;
318 	}
319 
320 	samplecnt_t  sample_rate                 = _session->nominal_sample_rate ();
321 	boost::optional<samplecnt_t> opt_samples = _session->available_capture_duration ();
322 
323 	Gtkmm2ext::set_source_rgb_a (cr, UIConfiguration::instance ().color ("widget:bg"), .7);
324 
325 	if (!opt_samples) {
326 		/* Available space is unknown */
327 		_layout_value->set_text (_("Unknown"));
328 	} else if (opt_samples.value_or (0) == max_samplecnt) {
329 		_layout_value->set_text (_(">24h"));
330 	} else {
331 		_rec_enabled_streams = 0;
332 		_session->foreach_route (this, &RemainInfoBox::count_recenabled_streams, false);
333 
334 		samplecnt_t samples = opt_samples.value_or (0);
335 
336 		if (_rec_enabled_streams > 0) {
337 			samples /= _rec_enabled_streams;
338 		}
339 
340 		float remain_sec = samples / (float)sample_rate;
341 		char buf[32];
342 
343 		if (remain_sec > 86400) {
344 			_layout_value->set_text (_(">24h"));
345 		} else if (remain_sec > 32400 /* 9 hours */) {
346 			snprintf (buf, sizeof (buf), "%.0f", remain_sec / 3600.f);
347 			_layout_value->set_text (std::string (buf) + S_("hours|h"));
348 		} else if (remain_sec > 5940 /* 99 mins */) {
349 			snprintf (buf, sizeof (buf), "%.1f", remain_sec / 3600.f);
350 			_layout_value->set_text (std::string (buf) + S_("hours|h"));
351 		} else if (remain_sec > 60*3 /* 3 mins */) {
352 			snprintf (buf, sizeof (buf), "%.0f", remain_sec / 60.f);
353 			_layout_value->set_text (std::string (buf) + S_("minutes|m"));
354 		} else {
355 			Gtkmm2ext::set_source_rgb_a (cr, UIConfiguration::instance ().color ("alert:red"), .7);
356 			snprintf (buf, sizeof (buf), "%.0f", remain_sec / 60.f);
357 			_layout_value->set_text (std::string (buf) + S_("minutes|m"));
358 		}
359 	}
360 
361 	/* draw box */
362 	Gtkmm2ext::rounded_rectangle (cr, 1 , 1, ww - 2, hh - 2, /*_height / 4.0 */ 4);
363 	cr->fill ();
364 
365 	/*draw text */
366 	Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("neutral:foreground"));
367 	cr->set_line_width (1.0);
368 
369 	int w, h;
370 	_layout_label->get_pixel_size (w, h);
371 	cr->move_to (.5 * (ww - w), 4);
372 	_layout_label->show_in_cairo_context (cr);
373 
374 	_layout_value->get_pixel_size (w, h);
375 	cr->move_to (.5 * (ww - w), hh - 4 - h);
376 	_layout_value->show_in_cairo_context (cr);
377 }
378 
379 void
update()380 RemainInfoBox::update ()
381 {
382 	RecInfoBox::update ();
383 }
384