]> git.decadent.org.uk Git - videolink.git/blob - generate_dvd.cpp
Switched to gtkmm 2.4+ since gtkmm 2.0 is being removed from Debian.
[videolink.git] / generate_dvd.cpp
1 // Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
2 // See the file "COPYING" for licence details.
3
4 #include <cerrno>
5 #include <cstring>
6 #include <fstream>
7 #include <iomanip>
8 #include <iostream>
9 #include <ostream>
10 #include <sstream>
11 #include <stdexcept>
12
13 #include <gdkmm/pixbuf.h>
14 #include <glibmm/miscutils.h>
15 #include <glibmm/spawn.h>
16
17 #include "dvd.hpp"
18 #include "generate_dvd.hpp"
19 #include "xml_utils.hpp"
20
21 namespace
22 {
23     // Return a closeness metric of an "end" rectangle to a "start"
24     // rectangle in the upward (-1) or downward (+1) direction.  Given
25     // several possible "end" rectangles, the one that seems visually
26     // closest in the given direction should have the highest value of
27     // this metric.  This is necessarily a heuristic function!    
28     double directed_closeness(const rectangle & start, const rectangle & end,
29                               int y_dir)
30     {
31         // The obvious approach is to use the centres of the
32         // rectangles.  However, for the "end" rectangle, using the
33         // horizontal position nearest the centre of the "start"
34         // rectangle seems to produce more reasonable results.  For
35         // example, if there are two "end" rectangles equally near to
36         // the "start" rectangle in terms of vertical distance and one
37         // of them horizontally overlaps the centre of the "start"
38         // rectangle, we want to pick that one even if the centre of
39         // that rectangle is further away from the centre of the
40         // "start" rectangle.
41         int start_x = (start.left + start.right) / 2;
42         int start_y = (start.top + start.bottom) / 2;
43         int end_y = (end.top + end.bottom) / 2;
44         int end_x;
45         if (end.right < start_x)
46             end_x = end.right;
47         else if (end.left > start_x)
48             end_x = end.left;
49         else
50             end_x = start_x;
51
52         // Return cosine of angle between the line between these points
53         // and the vertical, divided by the distance between the points
54         // if that is defined and positive; otherwise return 0.
55         int vertical_distance = (end_y - start_y) * y_dir;
56         if (vertical_distance <= 0)
57             return 0.0;
58         double distance_squared =
59             (end_x - start_x) * (end_x - start_x)
60             + (end_y - start_y) * (end_y - start_y);
61         return vertical_distance / distance_squared;
62     }
63
64     std::string temp_file_name(const temp_dir & dir,
65                                std::string base_name,
66                                unsigned index=0)
67     {
68         if (index != 0)
69         {
70             std::size_t index_pos = base_name.find("%3d");
71             assert(index_pos != std::string::npos);
72             base_name[index_pos] = '0' + index / 100;
73             base_name[index_pos + 1] = '0' + (index / 10) % 10;
74             base_name[index_pos + 2] = '0' + index % 10;
75         }
76
77         return Glib::build_filename(dir.get_name(), base_name);
78     }
79
80     // We would like to use just a single frame for the menu but this
81     // seems not to be legal or compatible.  The minimum length of a
82     // cell is 0.4 seconds but I've seen a static menu using 12 frames
83     // on a commercial "PAL" disc so let's use 12 frames regardless.
84     unsigned menu_duration_frames(const video::frame_params & params)
85     {
86         return 12;
87     }
88     double menu_duration_seconds(const video::frame_params & params)
89     {
90         return double(menu_duration_frames(params))
91             / double(params.rate_numer)
92             * double(params.rate_denom);
93     }
94
95     void throw_length_error(const char * limit_type, std::size_t limit)
96     {
97         std::ostringstream oss;
98         oss << "exceeded DVD limit: " << limit_type << " > " << limit;
99         throw std::length_error(oss.str());
100     }
101
102     // dvdauthor uses some menu numbers to represent entry points -
103     // distinct from the actual numbers of the menus assigned as those
104     // entry points - resulting in a practical limit of 119 per
105     // domain.  This seems to be an oddity of the parser that could be
106     // fixed, but for now we'll have to work with it.
107     const unsigned dvdauthor_anonymous_menus_max = dvd::domain_pgcs_max - 8;
108
109     // The current navigation code packs menu and button number into a
110     // single register, so the number of menus is limited to
111     // dvd::reg_s8_button_mult - 1 == 1023.  However temp_file_name()
112     // is limited to 999 numbered files and it seems pointless to
113     // change it to get another 24.
114     // If people really need more we could use separate menu and
115     // button number registers, possibly allowing up to 11900 menus
116     // (the size of the indirect jump tables might become a problem
117     // though).
118     const unsigned menus_max = 999;
119 }
120
121 dvd_generator::dvd_generator(const video::frame_params & frame_params,
122                              mpeg_encoder encoder)
123         : temp_dir_("videolink-"),
124           frame_params_(frame_params),
125           encoder_(encoder)
126 {}
127
128 dvd_generator::pgc_ref dvd_generator::add_menu()
129 {
130     pgc_ref next_menu(menu_pgc, menus_.size());
131
132     if (next_menu.index == menus_max)
133         throw_length_error("number of menus", menus_max);
134
135     menus_.resize(next_menu.index + 1);
136     return next_menu;
137 }
138
139 void dvd_generator::add_menu_entry(unsigned index,
140                                    const rectangle & area,
141                                    const pgc_ref & target)
142 {
143     assert(index < menus_.size());
144     assert(target.type == menu_pgc && target.index < menus_.size()
145            || target.type == title_pgc && target.index < titles_.size());
146
147     if (menus_[index].entries.size() == dvd::menu_buttons_max)
148         throw_length_error("number of buttons", dvd::menu_buttons_max);
149
150     menu_entry new_entry = { area, target };
151     menus_[index].entries.push_back(new_entry);
152 }
153
154 void dvd_generator::generate_menu_vob(unsigned index,
155                                       Glib::RefPtr<Gdk::Pixbuf> background,
156                                       Glib::RefPtr<Gdk::Pixbuf> highlights)
157     const
158 {
159     assert(index < menus_.size());
160     const menu & this_menu = menus_[index];
161
162     std::string background_name(
163         temp_file_name(temp_dir_, "menu-%3d-back.png", 1 + index));
164     std::cout << "saving " << background_name << std::endl;
165     background->save(background_name, "png");
166
167     std::string highlights_name(
168         temp_file_name(temp_dir_, "menu-%3d-links.png", 1 + index));
169     std::cout << "saving " << highlights_name << std::endl;
170     highlights->save(highlights_name, "png");
171
172     std::string spumux_name(
173         temp_file_name(temp_dir_, "menu-%3d.subpictures", 1 + index));
174     std::ofstream spumux_file(spumux_name.c_str());
175     spumux_file <<
176         "<subpictures>\n"
177         "  <stream>\n"
178         "    <spu force='yes' start='00:00:00.00'\n"
179         "        highlight='" << highlights_name << "'\n"
180         "        select='" << highlights_name << "'>\n";
181     int button_count = this_menu.entries.size();
182     for (int i = 0; i != button_count; ++i)
183     {
184         const menu_entry & this_entry = this_menu.entries[i];
185
186         // We program left and right to cycle through the buttons in
187         // the order the entries were added.  This should result in
188         // left and right behaving like the tab and shift-tab keys
189         // would in the browser.  Hopefully that's a sensible order.
190         // We program up and down to behave geometrically.
191         int up_button = i, down_button = i;
192         double up_closeness = 0.0, down_closeness = 0.0;
193         for (int j = 0; j != button_count; ++j)
194         {
195             const menu_entry & other_entry = this_menu.entries[j];
196             double closeness = directed_closeness(
197                 this_entry.area, other_entry.area, -1);
198             if (closeness > up_closeness)
199             {
200                 up_button = j;
201                 up_closeness = closeness;
202             }
203             else
204             {
205                 closeness = directed_closeness(
206                     this_entry.area, other_entry.area, 1);
207                 if (closeness > down_closeness)
208                 {
209                     down_button = j;
210                     down_closeness = closeness;
211                 }
212             }
213         }
214         spumux_file << "      <button"
215             " x0='" << this_entry.area.left << "'"
216             " y0='" << this_entry.area.top << "'"
217             " x1='" << this_entry.area.right << "'"
218             " y1='" << this_entry.area.bottom << "'"
219             " left='" << (i == 0 ? button_count : i) << "'"
220             " right='" << 1 + (i + 1) % button_count << "'"
221             " up='" << 1 + up_button << "'"
222             " down='" << 1 + down_button << "'"
223             "/>\n";
224     }
225     spumux_file <<
226         "    </spu>\n"
227         "  </stream>\n"
228         "</subpictures>\n";
229     spumux_file.close();
230     if (!spumux_file)
231         throw std::runtime_error("Failed to write control file for spumux");
232
233     std::ostringstream command_stream;
234     unsigned frame_count(menu_duration_frames(frame_params_));
235     if (encoder_ == mpeg_encoder_ffmpeg)
236     {
237         for (unsigned i = 0; i != frame_count; ++i)
238         {
239             std::string frame_name(background_name);
240             frame_name.push_back('-');
241             frame_name.push_back('0' + i / 10);
242             frame_name.push_back('0' + i % 10);
243             if (symlink(background_name.c_str(), frame_name.c_str()) != 0)
244                 throw std::runtime_error(
245                     std::string("symlink: ").append(std::strerror(errno)));
246         }
247         command_stream <<
248             "ffmpeg -f image2 -vcodec png"
249             " -r " << frame_params_.rate_numer <<
250             "/" << frame_params_.rate_denom <<
251             " -i " << background_name << "-%02d"
252             " -target " << frame_params_.common_name <<  "-dvd"
253             " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
254     }
255     else
256     {
257         assert(encoder_ == mpeg_encoder_mjpegtools_old
258                || encoder_ == mpeg_encoder_mjpegtools_new);
259         command_stream
260             << "pngtopnm " << background_name
261             << " | ppmtoy4m -v0 -n" << frame_count << " -F"
262             << frame_params_.rate_numer << ":" << frame_params_.rate_denom
263             << " -A" << frame_params_.pixel_ratio_width
264             << ":" << frame_params_.pixel_ratio_height
265             << " -Ip ";
266         // The chroma subsampling keywords changed between
267         // versions 1.6.2 and 1.8 of mjpegtools.  There is no
268         // keyword that works with both.
269         if (encoder_ == mpeg_encoder_mjpegtools_old)
270             command_stream << "-S420_mpeg2";
271         else
272             command_stream << "-S420mpeg2";
273         command_stream <<
274             " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
275             " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
276     }
277     command_stream
278         << " | spumux -v0 -mdvd " << spumux_name
279         << " > " << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index);
280     std::string command(command_stream.str());
281     const char * argv[] = {
282         "/bin/sh", "-c", command.c_str(), 0
283     };
284     std::cout << "running " << command << std::endl;
285     int command_result;
286     Glib::spawn_sync(".",
287                      Glib::ArrayHandle<std::string>(
288                          argv, sizeof(argv)/sizeof(argv[0]),
289                          Glib::OWNERSHIP_NONE),
290                      Glib::SPAWN_STDOUT_TO_DEV_NULL,
291                      sigc::slot<void>(),
292                      0, 0,
293                      &command_result);
294     if (command_result != 0)
295         throw std::runtime_error("spumux pipeline failed");
296 }
297
298 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
299 {
300     pgc_ref next_title(title_pgc, titles_.size());
301
302     // Check against maximum number of titles.
303     if (next_title.index == dvd::titles_max)
304         throw_length_error("number of titles", dvd::titles_max);
305
306     titles_.resize(next_title.index + 1);
307     titles_[next_title.index].swap(content);
308     return next_title;
309 }
310
311 void dvd_generator::generate(const std::string & output_dir) const
312 {
313     // This function uses a mixture of 0-based and 1-based numbering,
314     // due to the differing conventions of the language and the DVD
315     // format.  Variable names ending in "_index" indicate 0-based
316     // indices and variable names ending in "_num" indicate 1-based
317     // numbers.
318
319     std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
320     std::ofstream file(name.c_str());
321     file << "<dvdauthor>\n";
322
323     // We generate code that uses registers in the following way:
324     //
325     // g0: Scratch.
326     // g1: Target location when jumping between menus.  Top 6 bits are
327     //     the button number (like s8) and bottom 10 bits are the menu
328     //     number.  This is used for selecting the appropriate button
329     //     when entering a menu, for completing indirect jumps between
330     //     domains, and for jumping to the correct menu after exiting a
331     //     title.  This is set to 0 in the pre-routine of the target
332     //     menu.
333     // g2: Current location in menus.  This is used for jumping to the
334     //     correct menu when the player exits a title.
335     // g3: Target chapter number plus 1 when jumping to a title.
336     //     This is used to jump to the correct chapter and to
337     //     distinguish between indirect jumps to menus and titles.
338     //     This is set to 0 in the pre-routine of the target title.
339     // g4: Source menu location used to jump to a title.  This is
340     //     compared with g2 to determine whether to increment the
341     //     button number if the title is played to the end.
342
343     static const unsigned button_mult = dvd::reg_s8_button_mult;
344     static const unsigned menu_mask = button_mult - 1;
345     static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
346
347     // Iterate over VMGM and titlesets.  For these purposes, we
348     // consider the VMGM to be titleset 0.
349
350     // We need a titleset for each title, and we may also need titlesets to
351     // hold extra menus if we have too many for the VMGM.
352     // Also, we need at least one titleset.
353     const unsigned titleset_end = std::max<unsigned>(
354             1U + std::max<unsigned>(1U, titles_.size()),
355             (menus_.size() + dvdauthor_anonymous_menus_max - 1)
356                  / dvdauthor_anonymous_menus_max);
357
358     for (unsigned titleset_num = 0;
359          titleset_num != titleset_end;
360          ++titleset_num)
361     {
362         const char * const outer_element_name =
363             titleset_num == 0 ? "vmgm" : "titleset";
364         const bool have_real_title =
365             titleset_num != 0 && titleset_num <= titles_.size();
366         const bool have_real_menus =
367             titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
368
369         file << "  <" << outer_element_name << ">\n"
370              << "    <menus>\n";
371
372         const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
373         const unsigned menu_end =
374             have_real_menus
375             ? std::min((titleset_num + 1) * dvdauthor_anonymous_menus_max,
376                        menus_.size())
377             : menu_begin + 1;
378
379         for (unsigned menu_index = menu_begin;
380              menu_index != menu_end;
381              ++menu_index)
382         {
383             // There are various cases in which menus may be called:
384             //
385             // 1. The user follows a direct link to the menu.
386             // 2. The user follows an indirect link to some other menu
387             //    and that goes via this menu.  This is distinguished
388             //    from case 1 by the value of g1.  We must jump to or
389             //    at least toward the other menu.
390             // 3. The title menu is called when the disc is first
391             //    played or the user presses the "top menu" button.
392             //    This is distinguished from cases 2 and 3 by g1 == 0.
393             //    We make this look like case 1.
394             // 4. The root menu of a titleset is called when the user
395             //    follows an indirect link to the title.  This is
396             //    distinguished from all other cases by g3 != 0.  We
397             //    must jump to the title.
398             // 5. The root menu of a titleset is called when the title
399             //    ends or the user presses the "menu" button during
400             //    the title.  This is distinguished from cases 1, 2
401             //    and 4 by g1 == 0 and g3 == 0.  We must jump to the
402             //    latest menu (which can turn into case 1 or 2).
403             //
404             // Cases 3 and 5 do not apply to the same menus so they
405             // do not need to be distinguished.
406
407             if (menu_index == 0)
408             {
409                 // Title menu.
410                 file <<
411                     "      <pgc entry='title'>\n"
412                     "        <pre>\n"
413                     "          if (g1 eq 0)\n" // case 3
414                     "            g1 = " << 1 + button_mult << ";\n";
415             }
416             else if (menu_index == titleset_num * dvdauthor_anonymous_menus_max)
417             {
418                 // Root menu.
419                 file <<
420                     "      <pgc entry='root'>\n"
421                     "        <pre>\n";
422                 if (have_real_title)
423                 {
424                     file <<
425                         "          if (g3 ne 0)\n" // case 4
426                         "            jump title 1;\n"
427                         "          if (g1 eq 0) {\n" // case 5
428                         "            g1 = g2;\n"
429                         "            jump vmgm menu entry title;\n"
430                         "          }\n";
431                 }
432             }
433             else
434             {
435                 // Some other menu.
436                 file <<
437                     "      <pgc>\n"
438                     "        <pre>\n";
439             }
440
441             if (!have_real_menus)
442             {
443                 // This is a root menu only reachable from the title.
444                 file <<
445                     "        </pre>\n"
446                     "      </pgc>\n";
447                 continue;
448             }
449
450             const menu & this_menu = menus_[menu_index];
451
452             // Detect and handle case 2.
453             //
454             // There is a limit of 128 VM instructions in each PGC.
455             // Also, we can't jump to an arbitrary menu in another
456             // domain.  Finally, we can't do computed jumps.
457             // Therefore we statically expand and distribute a binary
458             // search across the menus, resulting in a code size of
459             // O(log(menu_count)) in each menu.  In practice there are
460             // at most 11 conditional jumps needed in any menu.
461             //
462             // The initial bounds of the binary search are strange
463             // because we must ensure that any jump between titlesets
464             // is to the first menu of the titleset, marked as the
465             // root entry.
466
467             // Mask target location to get the target menu.
468             file << "          g0 = g1 &amp; " << menu_mask << ";\n";
469
470             for (unsigned
471                      bottom = 0,
472                      top = 16 * dvdauthor_anonymous_menus_max;
473                  top - bottom > 1;)
474             {
475                 unsigned middle = (bottom + top) / 2;
476                 if (menu_index == bottom && middle < menus_.size())
477                 {
478                     file << "          if (g0 ge " << 1 + middle << ")\n"
479                          << "            jump ";
480                     unsigned target_titleset_num =
481                         middle / dvdauthor_anonymous_menus_max;
482                     if (target_titleset_num != titleset_num)
483                     {
484                         assert(middle % dvdauthor_anonymous_menus_max == 0);
485                         file << "titleset " << target_titleset_num
486                              << " menu entry root";
487                     }
488                     else
489                     {
490                         file << "menu "
491                              << 1 + middle % dvdauthor_anonymous_menus_max;
492                     }
493                     file << ";\n";
494                 }
495                 if (menu_index >= middle)
496                     bottom = middle;
497                 else
498                     top = middle;
499             }
500
501             // Case 1.
502
503             // Highlight the appropriate button.
504             file << "          s8 = g1 &amp; " << button_mask << ";\n";
505
506             // Copy the target location to the current location and
507             // then clear the target location so that the title menu
508             // can distinguish cases 2 and 3.
509             file <<
510                 "          g2 = g1;\n"
511                 "          g1 = 0;\n";
512
513             file <<
514                 "        </pre>\n"
515                 "        <vob file='"
516                  << temp_file_name(temp_dir_, "menu-%3d.mpeg",
517                                    1 + menu_index)
518                  << "'>\n"
519                 // Define a cell covering the whole menu and set a still
520                 // time at the end of that, since it seems all players
521                 // support that but some ignore a still time set on a PGC.
522                 "          <cell start='0' end='"
523                  << std::fixed << std::setprecision(4)
524                  << menu_duration_seconds(frame_params_) << "'"
525                 " chapter='yes' pause='inf'/>\n"
526                 "        </vob>\n";
527
528             for (unsigned button_index = 0;
529                  button_index != this_menu.entries.size();
530                  ++button_index)
531             {
532                 const pgc_ref & target =
533                     this_menu.entries[button_index].target;
534
535                 file << "        <button> ";
536
537                 if (target.type == menu_pgc)
538                 {
539                     unsigned target_button_num;
540
541                     if (target.sub_index)
542                     {
543                         target_button_num = target.sub_index;
544                     }
545                     else
546                     {
547                         // Look for a button on the new menu that links
548                         // back to this one.  If there is one, set that to
549                         // be the highlighted button; otherwise, use the
550                         // first button.
551                         const std::vector<menu_entry> & target_menu_entries =
552                             menus_[target.index].entries;
553                         pgc_ref this_pgc(menu_pgc, menu_index);
554                         target_button_num = target_menu_entries.size();
555                         while (target_button_num != 1
556                                && (target_menu_entries[target_button_num - 1].target
557                                    != this_pgc))
558                             --target_button_num;
559                     }
560                          
561                     // Set new menu location.
562                     file << "g1 = "
563                          << (1 + target.index + target_button_num * button_mult)
564                          << "; ";
565                     // Jump to the target menu.
566                     unsigned target_titleset_num =
567                         target.index / dvdauthor_anonymous_menus_max;
568                     if (target_titleset_num == titleset_num)
569                         file << "jump menu "
570                              << 1 + (target.index
571                                      % dvdauthor_anonymous_menus_max)
572                              << "; ";
573                     else if (target_titleset_num == 0)
574                         file << "jump vmgm menu entry title; ";
575                     else
576                         file << "jump titleset " << target_titleset_num
577                              << " menu entry root; ";
578                 }
579                 else
580                 {
581                     assert(target.type == title_pgc);
582
583                     // Record current menu location and set target chapter
584                     // number.
585                     file <<
586                         "g2 = " << (1 + menu_index
587                                     + (1 + button_index) * button_mult) << "; "
588                         "g3 = " << 1 + target.sub_index << "; ";
589                     // Jump to the target title, possibly via its titleset's
590                     // root menu.
591                     unsigned target_titleset_num = 1 + target.index;
592                     if (titleset_num == 0)
593                         file << "jump title " << target_titleset_num << "; ";
594                     else if (target_titleset_num == titleset_num)
595                         file << "jump title 1; ";
596                     else
597                         file << "jump titleset " << target_titleset_num
598                              << " menu entry root; ";
599                 }
600
601                 file <<  "</button>\n";
602             }
603
604             file <<
605                 "      </pgc>\n";
606         }
607
608         file << "    </menus>\n";
609
610         if (have_real_title)
611         {
612             file <<
613                 "    <titles>\n"
614                 "      <pgc>\n";
615
616             file << "        <pre>\n";
617
618             // Count chapters in the title.
619             unsigned n_chapters = 0;
620             for (vob_list::const_iterator
621                      it = titles_[titleset_num - 1].begin(),
622                      end = titles_[titleset_num - 1].end();
623                  it != end;
624                  ++it)
625             {
626                 // Chapter start times may be specified in the "chapters"
627                 // attribute as a comma-separated list.  If this is not
628                 // specified then the beginning of each file starts a new
629                 // chapter.  Thus the number of chapters in each file is
630                 // the number of commas in the chapter attribute, plus 1.
631                 ++n_chapters;
632                 std::size_t pos = 0;
633                 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
634                 {
635                     ++n_chapters;
636                     ++pos;
637                 }
638             }
639
640             // Move the chapter number to scratch so the root menu can
641             // distinguish cases 4 and 5.
642             file << "          g0 = g3; g3 = 0;\n";
643
644             // Copy the latest menu location for use by the post-routine.
645             file << "          g4 = g2;\n";
646
647             // Jump to the correct chapter.
648             for (unsigned chapter_num = 1;
649                  chapter_num <= n_chapters;
650                  ++chapter_num)
651                 file <<
652                     "          if (g0 eq " << 1 + chapter_num << ")\n"
653                     "            jump chapter " << chapter_num << ";\n";
654
655             file << "        </pre>\n";
656
657             for (vob_list::const_iterator
658                      it = titles_[titleset_num - 1].begin(),
659                      end = titles_[titleset_num - 1].end();
660                  it != end;
661                  ++it)
662             {
663                 file << "        <vob file='" << xml_escape(it->file) << "'";
664                 if (!it->chapters.empty())
665                     file << " chapters='" << xml_escape(it->chapters) << "'";
666                 if (!it->pause.empty())
667                     file << " pause='" << xml_escape(it->pause) << "'";
668                 file << "/>\n";
669             }
670
671             // If the user has not exited to the menus and then
672             // resumed the title, set the latest menu location to be
673             // the button after the one that linked to this title.
674             // In any case, return to the (root) menu which will
675             // then jump to the correct menu.
676             file <<
677                 "        <post>\n"
678                 "          if (g2 eq g4)\n"
679                 "            g2 = g2 + " << button_mult << ";\n"
680                 "          call menu;\n"
681                 "        </post>\n"
682                 "      </pgc>\n"
683                 "    </titles>\n";
684         }
685         else if (titleset_num != 0) // && !have_real_title
686         {
687             file << "    <titles><pgc/></titles>\n";
688         }
689
690         file << "  </" << outer_element_name << ">\n";
691     }
692
693     file << "</dvdauthor>\n";
694     file.close();
695
696     {
697         const char * argv[] = {
698             "dvdauthor",
699             "-o", output_dir.c_str(),
700             "-x", name.c_str(),
701             0
702         };
703         int command_result;
704         Glib::spawn_sync(".",
705                          Glib::ArrayHandle<std::string>(
706                              argv, sizeof(argv)/sizeof(argv[0]),
707                              Glib::OWNERSHIP_NONE),
708                          Glib::SPAWN_SEARCH_PATH
709                          | Glib::SPAWN_STDOUT_TO_DEV_NULL,
710                          sigc::slot<void>(),
711                          0, 0,
712                          &command_result);
713         if (command_result != 0)
714             throw std::runtime_error("dvdauthor failed");
715     }
716 }