Index: trunk/extensions/Score/Score.i18n.php |
— | — | @@ -40,12 +40,17 @@ |
41 | 41 | 'score-noabcinput' => 'ABC source file $1 could not be created.', |
42 | 42 | 'score-nofactory' => 'Failed to create LilyPond factory directory.', |
43 | 43 | 'score-noinput' => 'Failed to create LilyPond input file $1.', |
| 44 | + 'score-noogghandler' => 'Ogg/Vorbis conversion requires an installed and configured OggHandler extension, see https://www.mediawiki.org/wiki/Extension:OggHandler.', |
44 | 45 | 'score-nomidi' => 'No MIDI file generated despite being requested. If you are working in raw LilyPond mode, make sure to provide a proper \midi block.', |
45 | 46 | 'score-nooutput' => 'Failed to create LilyPond image directory $1.', |
46 | 47 | 'score-notexecutable' => 'Could not execute LilyPond: $1 is not an executable file. Make sure <code>$wgLilyPond</code> is set correctly.', |
| 48 | + 'score-novorbislink' => 'Unable to generate Ogg/Vorbis link: $1', |
| 49 | + 'score-oggconversionerr' => 'Unable to convert MIDI to Ogg/Vorbis: |
| 50 | +$1', |
47 | 51 | 'score-page' => 'Page $1', |
48 | 52 | 'score-pregreplaceerr' => 'PCRE regular expression replacement failed', |
49 | 53 | 'score-readerr' => 'Unable to read file $1.', |
| 54 | + 'score-timiditynotexecutable' => 'TiMidity++ could not be executed: $1 is not an executable file. Make sure <code>$wgTimidity</code> is set correctly.', |
50 | 55 | 'score-renameerr' => 'Error moving score files to upload directory.', |
51 | 56 | 'score-trimerr' => 'Image could not be trimmed: |
52 | 57 | $1 |
— | — | @@ -67,13 +72,17 @@ |
68 | 73 | 'score-noabcinput' => 'Displayed if an ABC source file could not be created for lang="ABC". $1 is the path to the file that could not be created.', |
69 | 74 | 'score-nofactory' => 'Displayed if the LilyPond/ImageMagick working directory cannot be created.', |
70 | 75 | 'score-noinput' => 'Displayed if the LilyPond input file cannot be created. $1 is the path to the input file.', |
| 76 | + 'score-noogghandler' => 'Displayed if Ogg/Vorbis rendering was requested without the OggHandler extension installed.', |
71 | 77 | 'score-nomidi' => 'Displayed if MIDI file generation was requested but no MIDI file was generated.', |
72 | 78 | 'score-nooutput' => 'Displayed if the LilyPond image/midi dir cannot be created. $1 is the name of the directory.', |
73 | 79 | 'score-notexecutable' => 'Displayed if LilyPond binary cannot be executed. $1 is the path to the LilyPond binary.', |
| 80 | + 'score-novorbislink' => 'Displayed if an Ogg/Vorbis link could not be generated. $1 is the explanation why.', |
| 81 | + 'score-oggconversionerr' => 'Displayed if the MIDI to Ogg/Vorbis conversion failed. $1 is the error (generally big block of text in a pre tag)', |
74 | 82 | 'score-page' => 'The word "Page" as used in pagination. $1 is the page number', |
75 | 83 | 'score-pregreplaceerr' => 'Displayed if a PCRE regular expression replacement failed.', |
76 | 84 | 'score-readerr' => 'Displayed if the extension could not read a file. $1 is the path to the file that could not be read.', |
77 | 85 | 'score-renameerr' => 'Displayed if moving the resultant files from the working environment to the upload directory fails.', |
| 86 | + 'score-timiditynotexecutable' => 'Displayed if TiMidity++ could not be executed. $1 is the path to the TiMidity++ binary.', |
78 | 87 | 'score-trimerr' => 'Displayed if the extension failed to trim an output image. $1 is the error (generally big block of text in a pre tag)', |
79 | 88 | 'score-versionerr' => 'Displayed if the extension failed to obtain the version string of LilyPond. $1 is the LilyPond stdout output generated by the attempt.', |
80 | 89 | ); |
Index: trunk/extensions/Score/Score.php |
— | — | @@ -45,15 +45,22 @@ |
46 | 46 | /* Whether to trim the score images. Requires ImageMagick. |
47 | 47 | * Default is $wgUseImageMagick and set in efScoreExtension */ |
48 | 48 | $wgScoreTrim = null; |
| 49 | + |
49 | 50 | /* Path of lilypond executable */ |
50 | 51 | if ( !isset( $wgLilyPond ) ) { |
51 | 52 | $wgLilyPond = '/usr/bin/lilypond'; |
52 | 53 | } |
| 54 | + |
53 | 55 | /* Path to converter from ABC */ |
54 | 56 | if ( !isset( $wgAbc2Ly ) ) { |
55 | 57 | $wgAbc2Ly = '/usr/bin/abc2ly'; |
56 | 58 | } |
57 | 59 | |
| 60 | +/* Path to TiMidity++ */ |
| 61 | +if ( !isset( $wgTimidity ) ) { |
| 62 | + $wgTimidity = '/usr/bin/timidity'; |
| 63 | +} |
| 64 | + |
58 | 65 | /* |
59 | 66 | * Extension credits |
60 | 67 | */ |
Index: trunk/extensions/Score/README |
— | — | @@ -7,9 +7,17 @@ |
8 | 8 | LilyPond installation. If you want the extension to trim the score files for |
9 | 9 | you, you will also need ImageMagick. |
10 | 10 | |
11 | | -This extension was tested with MediaWiki 1.18.0 and LilyPond 2.12.3. |
| 11 | +The extension is also capable of creating Ogg/Vorbis files from the MIDI files |
| 12 | +generated by LilyPond. If you want to make use of this functionality, you need |
| 13 | +to have the OggHandler extension installed, see |
12 | 14 | |
| 15 | +https://www.mediawiki.org/wiki/Extension:OggHandler |
13 | 16 | |
| 17 | +for more information. |
| 18 | + |
| 19 | +This extension was tested with MediaWiki 1.19alpha from SVN and LilyPond 2.12.3. |
| 20 | + |
| 21 | + |
14 | 22 | Setup |
15 | 23 | ===== |
16 | 24 | |
— | — | @@ -28,6 +36,8 @@ |
29 | 37 | $wgLilyPond = '/path/to/your/lilypond/executable'; /* required */ |
30 | 38 | $wgAbc2Ly = '/path/to/your/abc2ly/executable'; /* if you want ABC to |
31 | 39 | LilyPond conversion */ |
| 40 | + $wgTimidty = '/path/to/your/timidty/executable'; /* if you want MIDI to |
| 41 | + Vorbis conversion */ |
32 | 42 | $wgScoreTrim = true; /* Set to false if you don't want score trimming */ |
33 | 43 | |
34 | 44 | to your LocalSettings.php file. If you get unexpected out-of-memory errors, |
— | — | @@ -52,13 +62,22 @@ |
53 | 63 | <score midi="1">…</score> |
54 | 64 | |
55 | 65 | and the rendered image will be embedded into a hyperlink to an appropriate |
56 | | -MIDI file. For more complex scores, you may supply a complete lilypond file |
| 66 | +MIDI file. Use |
| 67 | + |
| 68 | +<score vorbis="1">…</score> |
| 69 | + |
| 70 | +and the rendered image will be embedded in an Ogg/Vorbis handler (provided you |
| 71 | +installed the OggHandler extension, see above under Prerequisites). Since the |
| 72 | +Vorbis file is created from the midi, the "vorbis" attribute implies the |
| 73 | +"midi" attribute. |
| 74 | + |
| 75 | +For more complex scores, you may supply a complete lilypond file |
57 | 76 | with |
58 | 77 | |
59 | 78 | <score raw="1">…</score> |
60 | 79 | |
61 | | -You may also combine the "raw" and "midi" attributes, but remember that in |
62 | | -this case, you need to provide the necessary \midi block yourself. |
| 80 | +You may also combine the "raw" and "midi"/"vorbis" attributes, but remember |
| 81 | +that in this case, you need to provide the necessary \midi block yourself. |
63 | 82 | |
64 | 83 | The extension also supports the ABC music notation through LilyPond's abc2ly |
65 | 84 | converter (see the documentation for $wgAbc2Ly above). Use |
Index: trunk/extensions/Score/Score.body.php |
— | — | @@ -67,6 +67,11 @@ |
68 | 68 | const LILYPOND_DIR_NAME = 'lilypond'; |
69 | 69 | |
70 | 70 | /** |
| 71 | + * Default audio player width. |
| 72 | + */ |
| 73 | + const DEFAULT_PLAYER_WIDTH = 300; |
| 74 | + |
| 75 | + /** |
71 | 76 | * Supported score languages. |
72 | 77 | */ |
73 | 78 | private static $supportedLangs = array( 'lilypond', 'ABC' ); |
— | — | @@ -163,8 +168,20 @@ |
164 | 169 | throw new ScoreException( wfMessage( 'score-invalidlang', $options['lang'] ) ); |
165 | 170 | } |
166 | 171 | |
| 172 | + /* Vorbis rendering? */ |
| 173 | + if ( array_key_exists( 'vorbis', $args ) ) { |
| 174 | + $options['vorbis'] = $args['vorbis']; |
| 175 | + } else { |
| 176 | + $options['vorbis'] = false; |
| 177 | + } |
| 178 | + if ( $options['vorbis'] && !( class_exists( 'OggHandler' ) && class_exists( 'OggAudioDisplay' ) ) ) { |
| 179 | + throw new ScoreException( wfMessage( 'score-noogghandler' ) ); |
| 180 | + } |
| 181 | + |
167 | 182 | /* Midi rendering? */ |
168 | | - if ( array_key_exists( 'midi', $args ) ) { |
| 183 | + if ( $options['vorbis'] ) { |
| 184 | + $options['midi'] = true; |
| 185 | + } elseif ( array_key_exists( 'midi', $args ) ) { |
169 | 186 | $options['midi'] = $args['midi']; |
170 | 187 | } else { |
171 | 188 | $options['midi'] = false; |
— | — | @@ -191,6 +208,7 @@ |
192 | 209 | * @param $code score code. |
193 | 210 | * @param $options array of music rendering options. Available options keys are: |
194 | 211 | * * lang: score language, |
| 212 | + * * vorbis: whether to create an Ogg/Vorbis file in an OggHandler, |
195 | 213 | * * midi: whether to link to a MIDI file, |
196 | 214 | * * raw: whether to assume raw LilyPond code. |
197 | 215 | * |
— | — | @@ -199,7 +217,7 @@ |
200 | 218 | * @throws ScoreException if an error occurs. |
201 | 219 | */ |
202 | 220 | private static function generateHTML( $code, $options ) { |
203 | | - global $wgUploadDirectory, $wgUploadPath; |
| 221 | + global $wgUploadDirectory, $wgUploadPath, $wgTmpDirectory, $wgOut; |
204 | 222 | |
205 | 223 | /* Various paths and file names */ |
206 | 224 | $cacheName = md5( $code ); /* always use MD5 of $code, regardless of language */ |
— | — | @@ -215,6 +233,8 @@ |
216 | 234 | $multiFormat = "$filePrefix-%d.png"; // for multi-page scores |
217 | 235 | $multiPathFormat = "$pathPrefix-%d.png"; |
218 | 236 | $multi1 = "$filePrefix-1.png"; |
| 237 | + $ogg = "$filePrefix.ogg"; |
| 238 | + $oggPath = "$pathPrefix.ogg"; |
219 | 239 | |
220 | 240 | /* Make sure $lilypondDir exists */ |
221 | 241 | if ( !file_exists( $lilypondDir ) ) { |
— | — | @@ -224,33 +244,57 @@ |
225 | 245 | } |
226 | 246 | } |
227 | 247 | |
228 | | - /* Generate PNG and MIDI files if necessary */ |
229 | | - if ( ( !file_exists( $image ) && !file_exists( $multi1 ) ) || ( $options['midi'] && !file_exists( $midi ) ) ) { |
230 | | - self::generatePngAndMidi( $code, $options, $filePrefix ); |
231 | | - } |
| 248 | + /* Generate working environment data */ |
| 249 | + $fuzz = md5( mt_rand() ); |
| 250 | + $factoryDirectory = $wgTmpDirectory . "/MWLP.$fuzz"; |
232 | 251 | |
233 | | - /* return output link(s) */ |
234 | | - if ( file_exists( $image ) ) { |
235 | | - $link = Html::rawElement( 'img', array( |
236 | | - 'src' => $imagePath, |
237 | | - 'alt' => $code, |
238 | | - ) ); |
239 | | - } elseif ( file_exists( $multi1 ) ) { |
240 | | - $link = ''; |
241 | | - for ( $i = 1; file_exists( sprintf( $multiFormat, $i ) ); ++$i ) { |
242 | | - $link .= Html::rawElement( 'img', array( |
243 | | - 'src' => sprintf( $multiPathFormat, $i ), |
244 | | - 'alt' => wfMessage( 'score-page' )->inContentLanguage()->numParams( $i )->plain() |
| 252 | + try { |
| 253 | + /* Generate PNG and MIDI files if necessary */ |
| 254 | + if ( ( !file_exists( $image ) && !file_exists( $multi1 ) ) || ( $options['midi'] && !file_exists( $midi ) ) ) { |
| 255 | + self::generatePngAndMidi( $code, $options, $filePrefix, $factoryDirectory ); |
| 256 | + } |
| 257 | + |
| 258 | + /* Generate Ogg/Vorbis file if necessary */ |
| 259 | + if ( $options['vorbis'] && !file_exists( $ogg ) ) { |
| 260 | + self::generateOgg( $options, $filePrefix, $factoryDirectory ); |
| 261 | + } |
| 262 | + |
| 263 | + /* return output link(s) */ |
| 264 | + if ( file_exists( $image ) ) { |
| 265 | + $link = Html::rawElement( 'img', array( |
| 266 | + 'src' => $imagePath, |
| 267 | + 'alt' => $code, |
245 | 268 | ) ); |
| 269 | + } elseif ( file_exists( $multi1 ) ) { |
| 270 | + $link = ''; |
| 271 | + for ( $i = 1; file_exists( sprintf( $multiFormat, $i ) ); ++$i ) { |
| 272 | + $link .= Html::rawElement( 'img', array( |
| 273 | + 'src' => sprintf( $multiPathFormat, $i ), |
| 274 | + 'alt' => wfMessage( 'score-page' )->inContentLanguage()->numParams( $i )->plain() |
| 275 | + ) ); |
| 276 | + } |
| 277 | + } else { |
| 278 | + /* No images; this may actually happen in raw lilypond mode */ |
| 279 | + self::debug( "No output images $image or $multi1!\n" ); |
| 280 | + $link = 'No image'; |
246 | 281 | } |
247 | | - } else { |
248 | | - /* No images; this may actually happen in raw lilypond mode */ |
249 | | - self::debug( "No output images $image or $multi1!\n" ); |
250 | | - $link = 'No image'; |
| 282 | + if ( $options['midi'] ) { |
| 283 | + $link = Html::rawElement( 'a', array( 'href' => $midiPath ), $link ); |
| 284 | + } |
| 285 | + if ( $options['vorbis'] ) { |
| 286 | + try { |
| 287 | + $oh = new OggHandler(); |
| 288 | + $oh->setHeaders( $wgOut ); |
| 289 | + $oad = new OggAudioDisplay( new UnregisteredLocalFile( false, false, $ogg ), $oggPath, self::DEFAULT_PLAYER_WIDTH, 0, 0, $oggPath, false ); |
| 290 | + $link .= $oad->toHtml( array( 'alt' => $code ) ); |
| 291 | + } catch ( Exception $e ) { |
| 292 | + throw new ScoreException( wfMessage( 'score-novorbislink', $e->getMessage() ), 0, $e ); |
| 293 | + } |
| 294 | + } |
| 295 | + } catch ( Exception $e ) { |
| 296 | + self::eraseFactory( $factoryDirectory ); |
| 297 | + throw $e; |
251 | 298 | } |
252 | | - if ( $options['midi'] ) { |
253 | | - $link = Html::rawElement( 'a', array( 'href' => $midiPath ), $link ); |
254 | | - } |
255 | 299 | |
256 | 300 | return $link; |
257 | 301 | } |
— | — | @@ -261,11 +305,12 @@ |
262 | 306 | * @param $code score code. |
263 | 307 | * @param $options rendering options, see Score::generateHTML() for explanation. |
264 | 308 | * @param $filePrefix prefix for the generated files. |
| 309 | + * @param $factoryDirectory directory of the working environment. |
265 | 310 | * |
266 | 311 | * @throws ScoreException on error. |
267 | 312 | */ |
268 | | - private static function generatePngAndMidi( $code, $options, $filePrefix ) { |
269 | | - global $wgTmpDirectory, $wgLilyPond, $wgScoreTrim; |
| 313 | + private static function generatePngAndMidi( $code, $options, $filePrefix, $factoryDirectory ) { |
| 314 | + global $wgLilyPond, $wgScoreTrim; |
270 | 315 | |
271 | 316 | wfProfileIn( __METHOD__ ); |
272 | 317 | |
— | — | @@ -291,9 +336,7 @@ |
292 | 337 | throw new ScoreException( wfMessage( 'score-cleanerr' ) ); |
293 | 338 | } |
294 | 339 | |
295 | | - /* Create a working environment */ |
296 | | - $fuzz = md5( mt_rand() ); |
297 | | - $factoryDirectory = $wgTmpDirectory . "/MWLP.$fuzz"; |
| 340 | + /* Create the working environment */ |
298 | 341 | self::createFactory( $factoryDirectory ); |
299 | 342 | $factoryLy = "$factoryDirectory/file.ly"; |
300 | 343 | $factoryMidi = "$factoryDirectory/file.midi"; |
— | — | @@ -383,13 +426,10 @@ |
384 | 427 | throw new ScoreException( wfMessage( 'score-renameerr' ) ); |
385 | 428 | } |
386 | 429 | } catch ( Exception $e ) { |
387 | | - self::eraseFactory( $factoryDirectory ); |
388 | 430 | wfProfileOut( __METHOD__ ); |
389 | 431 | throw $e; |
390 | 432 | } |
391 | 433 | |
392 | | - /* tear down working environment */ |
393 | | - self::eraseFactory( $factoryDirectory ); |
394 | 434 | wfProfileOut( __METHOD__ ); |
395 | 435 | } |
396 | 436 | |
— | — | @@ -429,6 +469,54 @@ |
430 | 470 | } |
431 | 471 | |
432 | 472 | /** |
| 473 | + * Generates an Ogg/Vorbis file from a MIDI file using timidity. |
| 474 | + * |
| 475 | + * @param $options rendering options, see Score::generateHTML() for explanation. |
| 476 | + * @param $filePrefix prefix for the generated Ogg file. |
| 477 | + * @param $factoryDirectory directory of the working environment. |
| 478 | + * |
| 479 | + * @throws ScoreException if an error occurs. |
| 480 | + */ |
| 481 | + private static function generateOgg( $options, $filePrefix, $factoryDirectory ) { |
| 482 | + global $wgTimidity; |
| 483 | + |
| 484 | + wfProfileIn( __METHOD__ ); |
| 485 | + |
| 486 | + try { |
| 487 | + /* Working environment */ |
| 488 | + self::createFactory( $factoryDirectory ); |
| 489 | + $factoryOgg = "$factoryDirectory/file.ogg"; |
| 490 | + $midi = "$filePrefix.midi"; |
| 491 | + $ogg = "$filePrefix.ogg"; |
| 492 | + |
| 493 | + /* Run timidity */ |
| 494 | + if ( !is_executable( $wgTimidity ) ) { |
| 495 | + throw new ScoreException( wfMessage( 'score-timiditynotexecutable', $wgTimidity ) ); |
| 496 | + } |
| 497 | + $cmd = wfEscapeShellArg( $wgTimidity ) |
| 498 | + . ' -Ov' // Vorbis output |
| 499 | + . ' --output-file=' . wfEscapeShellArg( $factoryOgg ) |
| 500 | + . ' ' . wfEscapeShellArg( $midi ) |
| 501 | + . ' 2>&1'; |
| 502 | + $output = wfShellExec( $cmd, $rc ); |
| 503 | + if ( ( $rc != 0 ) || !file_exists( $factoryOgg ) ) { |
| 504 | + self::throwCallException( wfMessage( 'score-oggconversionerr' ), $output ); |
| 505 | + } |
| 506 | + |
| 507 | + /* Move resultant file to proper place */ |
| 508 | + $rc = rename( $factoryOgg, $ogg ); |
| 509 | + if ( !$rc ) { |
| 510 | + throw new ScoreException( wfMessage( 'score-renameerr' ) ); |
| 511 | + } |
| 512 | + } catch ( Exception $e ) { |
| 513 | + wfProfileOut( __METHOD__ ); |
| 514 | + throw $e; |
| 515 | + } |
| 516 | + |
| 517 | + wfProfileOut( __METHOD__ ); |
| 518 | + } |
| 519 | + |
| 520 | + /** |
433 | 521 | * Generates LilyPond code. |
434 | 522 | * |
435 | 523 | * @param $code score code. |