Mercurial > audlegacy-plugins
comparison src/flacng/plugin.c @ 930:2f742d127b3e trunk
[svn] - initial import of flacng from audacious-flacng-0.012
author | nenolod |
---|---|
date | Mon, 09 Apr 2007 10:55:23 -0700 |
parents | |
children | b6c95e2a14f4 |
comparison
equal
deleted
inserted
replaced
929:9631824411bf | 930:2f742d127b3e |
---|---|
1 /* | |
2 * A FLAC decoder plugin for the Audacious Media Player | |
3 * Copyright (C) 2005 Ralf Ertzinger | |
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 | |
16 * along with this program; if not, write to the Free Software | |
17 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
18 */ | |
19 | |
20 #include <audacious/util.h> | |
21 #include <audacious/output.h> | |
22 #include <glib/gi18n.h> | |
23 #include "flacng.h" | |
24 #include "tools.h" | |
25 #include "plugin.h" | |
26 #include "seekable_stream_callbacks.h" | |
27 #include "version.h" | |
28 #include "debug.h" | |
29 | |
30 static gchar *flac_fmts[] = { "flac", NULL }; | |
31 | |
32 InputPlugin flac_ip = { | |
33 NULL, | |
34 NULL, | |
35 "FLACng Audio Plugin", | |
36 flac_init, | |
37 flac_aboutbox, | |
38 NULL, | |
39 flac_is_our_file, | |
40 NULL, | |
41 flac_play_file, | |
42 flac_stop, | |
43 flac_pause, | |
44 flac_seek, | |
45 NULL, | |
46 NULL, | |
47 NULL, | |
48 NULL, | |
49 NULL, | |
50 NULL, | |
51 NULL, | |
52 NULL, | |
53 NULL, | |
54 flac_get_song_info, | |
55 NULL, | |
56 NULL, | |
57 flac_get_song_tuple, // get a tuple | |
58 NULL, | |
59 NULL, // write a tuple back to a file as a tag | |
60 /* flac_is_our_fd */ NULL, // version of is_our_file which is handed an FD | |
61 flac_fmts // vector of fileextensions allowed by the plugin | |
62 }; | |
63 | |
64 FLAC__StreamDecoder* test_decoder; | |
65 FLAC__StreamDecoder* main_decoder; | |
66 callback_info* test_info; | |
67 callback_info* main_info; | |
68 gboolean plugin_initialized = FALSE; | |
69 gint seek_to = -1; | |
70 static GThread* thread; | |
71 GMutex* flac_pl_mutex; | |
72 | |
73 /* === */ | |
74 | |
75 InputPlugin* get_iplugin_info(void) { | |
76 | |
77 _ENTER; | |
78 | |
79 _DEBUG("%s (%s)", flac_ip.description, _VERSION); | |
80 | |
81 _LEAVE &flac_ip; | |
82 } | |
83 | |
84 /* --- */ | |
85 | |
86 void flac_init(void) { | |
87 | |
88 FLAC__StreamDecoderInitStatus ret; | |
89 | |
90 _ENTER; | |
91 | |
92 /* | |
93 * Callback structure and decoder for file test | |
94 * purposes | |
95 */ | |
96 if (NULL == (test_info = init_callback_info("test"))) { | |
97 _ERROR("Could not initialize the test callback structure!"); | |
98 _LEAVE; | |
99 } | |
100 _DEBUG("Test callback structure at %p", test_info); | |
101 | |
102 if (NULL == (test_decoder = FLAC__stream_decoder_new())) { | |
103 _ERROR("Could not create the test FLAC decoder instance!"); | |
104 _LEAVE; | |
105 } | |
106 | |
107 | |
108 /* | |
109 * We want the VORBISCOMMENT metadata, for the file tags | |
110 * and SEEKTABLE | |
111 */ | |
112 FLAC__stream_decoder_set_metadata_respond(test_decoder, FLAC__METADATA_TYPE_VORBIS_COMMENT); | |
113 FLAC__stream_decoder_set_metadata_respond(test_decoder, FLAC__METADATA_TYPE_SEEKTABLE); | |
114 | |
115 /* | |
116 * Callback structure and decoder for main decoding | |
117 * loop | |
118 */ | |
119 if (NULL == (main_info = init_callback_info("main"))) { | |
120 _ERROR("Could not initialize the main callback structure!"); | |
121 _LEAVE; | |
122 } | |
123 _DEBUG("Main callback structure at %p", main_info); | |
124 | |
125 if (NULL == (main_decoder = FLAC__stream_decoder_new())) { | |
126 _ERROR("Could not create the main FLAC decoder instance!"); | |
127 _LEAVE; | |
128 } | |
129 | |
130 | |
131 /* | |
132 * We want the VORBISCOMMENT metadata, for the file tags | |
133 * and SEEKTABLE | |
134 */ | |
135 FLAC__stream_decoder_set_metadata_respond(main_decoder, FLAC__METADATA_TYPE_VORBIS_COMMENT); | |
136 FLAC__stream_decoder_set_metadata_respond(main_decoder, FLAC__METADATA_TYPE_SEEKTABLE); | |
137 | |
138 /* | |
139 * Initialize decoders | |
140 */ | |
141 if (FLAC__STREAM_DECODER_INIT_STATUS_OK != (ret = FLAC__stream_decoder_init_stream( | |
142 test_decoder, | |
143 read_callback, | |
144 seek_callback, | |
145 tell_callback, | |
146 length_callback, | |
147 eof_callback, | |
148 write_callback, | |
149 metadata_callback, | |
150 error_callback, | |
151 test_info))) { | |
152 _ERROR("Could not initialize test FLAC decoder: %s(%d)", StreamDecoderInitState(ret), ret); | |
153 _LEAVE; | |
154 } | |
155 | |
156 if (FLAC__STREAM_DECODER_INIT_STATUS_OK != (ret = FLAC__stream_decoder_init_stream( | |
157 main_decoder, | |
158 read_callback, | |
159 seek_callback, | |
160 tell_callback, | |
161 length_callback, | |
162 eof_callback, | |
163 write_callback, | |
164 metadata_callback, | |
165 error_callback, | |
166 main_info))) { | |
167 _ERROR("Could not initialize main FLAC decoder: %s(%d)", StreamDecoderInitState(ret), ret); | |
168 _LEAVE; | |
169 } | |
170 | |
171 /* | |
172 * Initialize the play loop mutex | |
173 */ | |
174 flac_pl_mutex = g_mutex_new(); | |
175 | |
176 _DEBUG("plugin initialized OK!"); | |
177 plugin_initialized = TRUE; | |
178 _LEAVE; | |
179 } | |
180 | |
181 /* --- */ | |
182 | |
183 gboolean flac_is_our_file(gchar* filename) { | |
184 | |
185 _ENTER; | |
186 | |
187 if (!plugin_initialized) { | |
188 _ERROR("Plugin not initialized!"); | |
189 _LEAVE FALSE; | |
190 } | |
191 | |
192 _DEBUG("Testing file: %s", filename); | |
193 | |
194 if (FALSE == read_metadata(filename, test_decoder, test_info)) { | |
195 _DEBUG("File not handled by this plugin!"); | |
196 _LEAVE FALSE; | |
197 } | |
198 | |
199 /* | |
200 * See if the metadata has changed | |
201 */ | |
202 if (FALSE == test_info->metadata_changed) { | |
203 _DEBUG("No metadata found in stream"); | |
204 _LEAVE FALSE; | |
205 } | |
206 | |
207 /* | |
208 * If we get here, the file is supported by FLAC. | |
209 * The stream characteristics have been filled in by | |
210 * the metadata callback. | |
211 * We can close the stream now. | |
212 */ | |
213 | |
214 vfs_fclose(test_info->input_stream); | |
215 test_info->input_stream = NULL; | |
216 | |
217 | |
218 _DEBUG("Stream encoded at %d Hz, %d bps, %d channels", | |
219 test_info->stream.samplerate, | |
220 test_info->stream.bits_per_sample, | |
221 test_info->stream.channels); | |
222 | |
223 if (MAX_SUPPORTED_CHANNELS < test_info->stream.channels) { | |
224 _ERROR("This number of channels (%d) is currently not supported, stream not handled by this plugin", | |
225 test_info->stream.channels); | |
226 _LEAVE FALSE; | |
227 } | |
228 | |
229 if ((16 != test_info->stream.bits_per_sample) && | |
230 (24 != test_info->stream.bits_per_sample) && | |
231 (8 != test_info->stream.bits_per_sample)) { | |
232 _ERROR("This number of bits (%d) is currently not supported, stream not handled by this plugin", | |
233 test_info->stream.bits_per_sample); | |
234 _LEAVE FALSE; | |
235 } | |
236 | |
237 /* | |
238 * Looks good. | |
239 */ | |
240 | |
241 _DEBUG("Accepting file %s", filename); | |
242 | |
243 reset_info(test_info); | |
244 | |
245 _LEAVE TRUE; | |
246 } | |
247 | |
248 /* --- */ | |
249 | |
250 void squeeze_audio(gint32* src, void* dst, guint count, guint src_res, guint dst_res) { | |
251 | |
252 /* | |
253 * Changes the sample width of count samples in src from | |
254 * src_res to dst_res and places the result in dst | |
255 */ | |
256 | |
257 gint i; | |
258 gint32* rp = src; | |
259 gint8* wp = dst; | |
260 gint16* wp2 = dst; | |
261 gint32* wp4 = dst; | |
262 | |
263 _ENTER; | |
264 | |
265 _DEBUG("Converting %d samples %d->%d", count, src_res, dst_res); | |
266 | |
267 if ((src_res % 8 != 0) || (dst_res % 8 != 0)) { | |
268 _ERROR("Can not convert from %d bps to %d bps: not a multiple of 8", | |
269 src_res, dst_res); | |
270 _LEAVE; | |
271 } | |
272 | |
273 if (16 == dst_res) { | |
274 if (8 == src_res) { | |
275 for (i=0; i<count; i++, wp2++, rp++) { | |
276 *wp2 = (*rp << 8) & 0xffff; | |
277 } | |
278 } else if (16 == src_res) { | |
279 for (i=0; i<count; i++, wp2++, rp++) { | |
280 *wp2 = *rp & 0xffff; | |
281 } | |
282 } else if (24 == src_res) { | |
283 for (i=0; i<count; i++, wp2++, rp++) { | |
284 *wp2 = (*rp >> 8) & 0xffff; | |
285 } | |
286 } else if (32 == src_res) { | |
287 for (i=0; i<count; i++, wp2++, rp++) { | |
288 *wp2 = (*rp >> 16) & 0xffff; | |
289 } | |
290 } | |
291 } else if (8 == dst_res) { | |
292 if (8 == src_res) { | |
293 for (i=0; i<count; i++, wp++, rp++) { | |
294 *wp = *rp & 0xff; | |
295 } | |
296 } else if (16 == src_res) { | |
297 for (i=0; i<count; i++, wp++, rp++) { | |
298 *wp = (*rp >> 8) & 0xff; | |
299 } | |
300 } else if (24 == src_res) { | |
301 for (i=0; i<count; i++, wp++, rp++) { | |
302 *wp = (*rp >> 16) & 0xff; | |
303 } | |
304 } else if (32 == src_res) { | |
305 for (i=0; i<count; i++, wp++, rp++) { | |
306 *wp = (*rp >> 24) & 0xff; | |
307 } | |
308 } | |
309 } else if (32 == dst_res) { | |
310 if (8 == src_res) { | |
311 for (i=0; i<count; i++, wp4++, rp++) { | |
312 *wp4 = (*rp << 24) & 0xffffffff; | |
313 } | |
314 } else if (16 == src_res) { | |
315 for (i=0; i<count; i++, wp4++, rp++) { | |
316 *wp4 = (*rp << 16) & 0xffffffff; | |
317 } | |
318 } else if (24 == src_res) { | |
319 for (i=0; i<count; i++, wp4++, rp++) { | |
320 *wp4 = (*rp << 8) & 0xffffffff; | |
321 } | |
322 } else if (32 == src_res) { | |
323 for (i=0; i<count; i++, wp4++, rp++) { | |
324 *wp4 = *rp; | |
325 } | |
326 } | |
327 } | |
328 | |
329 _LEAVE; | |
330 } | |
331 | |
332 /* --- */ | |
333 | |
334 static gpointer flac_play_loop(gpointer arg) { | |
335 | |
336 /* | |
337 * The main play loop. | |
338 * Decode a frame, push the decoded data to the output plugin | |
339 * chunkwise. Repeat until finished. | |
340 * | |
341 * Must be entered with flac_pl_mutex held! | |
342 */ | |
343 | |
344 gint ofree; | |
345 gint32* read_pointer; | |
346 gint elements_left; | |
347 gint seek_sample; | |
348 FLAC__StreamDecoderState state; | |
349 struct stream_info stream_info; | |
350 guint sample_count; | |
351 void* play_buffer; | |
352 InputPlayback* playback = (InputPlayback *) arg; | |
353 | |
354 _ENTER; | |
355 | |
356 if (NULL == (play_buffer = malloc(BUFFER_SIZE_BYTE))) { | |
357 _ERROR("Could not allocate conversion buffer"); | |
358 playback->playing = FALSE; | |
359 g_mutex_unlock(flac_pl_mutex); | |
360 _LEAVE NULL; | |
361 } | |
362 | |
363 stream_info.samplerate = main_info->stream.samplerate; | |
364 stream_info.channels = main_info->stream.channels; | |
365 main_info->metadata_changed = FALSE; | |
366 | |
367 if (!playback->output->open_audio(FMT_S16_NE, | |
368 main_info->stream.samplerate, | |
369 main_info->stream.channels)) { | |
370 playback->playing = FALSE; | |
371 _ERROR("Could not open output plugin!"); | |
372 g_mutex_unlock(flac_pl_mutex); | |
373 _LEAVE NULL; | |
374 } | |
375 | |
376 while (TRUE == playback->playing) { | |
377 | |
378 /* | |
379 * Try to decode a single frame of audio | |
380 */ | |
381 if (FALSE == FLAC__stream_decoder_process_single(main_decoder)) { | |
382 /* | |
383 * Something went wrong | |
384 */ | |
385 _ERROR("Error while decoding!"); | |
386 break; | |
387 } | |
388 | |
389 /* | |
390 * Maybe the metadata changed midstream. Check. | |
391 */ | |
392 | |
393 if (main_info->metadata_changed) { | |
394 /* | |
395 * Samplerate and channels are important | |
396 */ | |
397 if (stream_info.samplerate != main_info->stream.samplerate) { | |
398 _ERROR("Samplerate changed midstream (now: %d, was: %d). This is not supported yet.", | |
399 main_info->stream.samplerate, stream_info.samplerate); | |
400 break; | |
401 } | |
402 | |
403 if (stream_info.channels != main_info->stream.channels) { | |
404 _ERROR("Number of channels changed midstream (now: %d, was: %d). This is not supported yet.", | |
405 main_info->stream.channels, stream_info.channels); | |
406 break; | |
407 } | |
408 | |
409 main_info->metadata_changed = FALSE; | |
410 } | |
411 | |
412 /* | |
413 * Compare the frame metadata to the current stream metadata | |
414 */ | |
415 if (main_info->stream.samplerate != main_info->frame.samplerate) { | |
416 _ERROR("Frame samplerate mismatch (stream: %d, frame: %d)!", | |
417 main_info->stream.samplerate, main_info->frame.samplerate); | |
418 break; | |
419 } | |
420 | |
421 if (main_info->stream.channels != main_info->frame.channels) { | |
422 _ERROR("Frame channel mismatch (stream: %d, frame: %d)!", | |
423 main_info->stream.channels, main_info->frame.channels); | |
424 break; | |
425 } | |
426 | |
427 /* | |
428 * If the frame decoded was an audio frame we now | |
429 * have data in info->output_buffer | |
430 * | |
431 * The data in this buffer is in 32 bit wide samples, even if the | |
432 * real sample width is smaller. It has to be converted before | |
433 * feeding it to he output plugin. | |
434 * | |
435 * Feed the data in chunks of OUTPUT_BLOCK_SIZE elements to the output | |
436 * plugin | |
437 */ | |
438 read_pointer = main_info->output_buffer; | |
439 elements_left = main_info->buffer_used; | |
440 | |
441 while ((TRUE == playback->playing) && (0 != elements_left)) { | |
442 | |
443 sample_count = MIN(OUTPUT_BLOCK_SIZE, elements_left); | |
444 | |
445 /* | |
446 * Convert the audio. | |
447 * Currently this is hardcoded to 16 bit. | |
448 * 8 and 24 bit are sampled up/down linear. | |
449 */ | |
450 _DEBUG("Converting %d samples to FMT_S16_NE", sample_count); | |
451 squeeze_audio(read_pointer, play_buffer, sample_count, main_info->frame.bits_per_sample, 16); | |
452 | |
453 _DEBUG("Copying %d samples to output plugin", sample_count); | |
454 | |
455 produce_audio(flac_ip.output->written_time(), FMT_S16_NE, main_info->frame.channels, sample_count * sizeof(gint16), play_buffer, NULL); | |
456 | |
457 read_pointer += sample_count; | |
458 elements_left -= sample_count; | |
459 | |
460 _DEBUG("%d elements left to be output", elements_left); | |
461 } | |
462 | |
463 /* | |
464 * Clear the buffer. | |
465 */ | |
466 main_info->write_pointer = main_info->output_buffer; | |
467 main_info->buffer_free = BUFFER_SIZE_SAMP; | |
468 main_info->buffer_used = 0; | |
469 | |
470 /* | |
471 * Do we have to seek to somewhere? | |
472 */ | |
473 if (-1 != seek_to) { | |
474 _DEBUG("Seek requested to %d seconds", seek_to); | |
475 | |
476 if (FALSE == main_info->stream.has_seektable) { | |
477 _ERROR("Stream does not have a seektable, can not seek!"); | |
478 } else { | |
479 seek_sample = seek_to * main_info->stream.samplerate; | |
480 _DEBUG("Seek requested to sample %d", seek_sample); | |
481 if (FALSE == FLAC__stream_decoder_seek_absolute(main_decoder, seek_sample)) { | |
482 _ERROR("Could not seek to sample %d!", seek_sample); | |
483 } else { | |
484 /* | |
485 * Flush the buffers | |
486 */ | |
487 flac_ip.output->flush(seek_to * 1000); | |
488 } | |
489 } | |
490 seek_to = -1; | |
491 } | |
492 | |
493 /* | |
494 * Have we reached the end of the stream? | |
495 */ | |
496 state = FLAC__stream_decoder_get_state(main_decoder); | |
497 if (0 == elements_left && (FLAC__STREAM_DECODER_END_OF_STREAM == state)) { | |
498 /* | |
499 * Yes. Drain the output buffer and stop playing. | |
500 */ | |
501 | |
502 _DEBUG("End of stream reached, draining output buffer"); | |
503 | |
504 while((-1 == seek_to) && flac_ip.output->buffer_playing() && playback->playing == TRUE) { | |
505 g_usleep(40000); | |
506 } | |
507 | |
508 if (-1 == seek_to) { | |
509 _DEBUG("Output buffer empty."); | |
510 playback->playing = FALSE; | |
511 } | |
512 } | |
513 } | |
514 | |
515 /* | |
516 * Clean up a bit | |
517 */ | |
518 playback->playing = FALSE; | |
519 _DEBUG("Closing audio device"); | |
520 flac_ip.output->close_audio(); | |
521 _DEBUG("Audio device closed"); | |
522 | |
523 free(play_buffer); | |
524 | |
525 if (FALSE == FLAC__stream_decoder_flush(main_decoder)) { | |
526 _ERROR("Could not flush decoder state!"); | |
527 } | |
528 | |
529 /* | |
530 * Release the play loop mutex | |
531 */ | |
532 g_mutex_unlock(flac_pl_mutex); | |
533 | |
534 g_thread_exit(NULL); | |
535 | |
536 _LEAVE NULL; | |
537 } | |
538 | |
539 /* --- */ | |
540 | |
541 void flac_play_file (InputPlayback* input) { | |
542 | |
543 gint l; | |
544 | |
545 _ENTER; | |
546 | |
547 if (!plugin_initialized) { | |
548 _ERROR("plugin not initialized!"); | |
549 _LEAVE; | |
550 } | |
551 | |
552 /* | |
553 * This will end a currently running decoder thread | |
554 */ | |
555 input->playing = FALSE; | |
556 xmms_usleep(20000); | |
557 | |
558 if (FALSE == read_metadata(input->filename, main_decoder, main_info)) { | |
559 _ERROR("Could not prepare file for playing!"); | |
560 _LEAVE; | |
561 } | |
562 | |
563 /* | |
564 * Calculate the length | |
565 */ | |
566 if (0 == main_info->stream.samplerate) { | |
567 _ERROR("Invalid sample rate for stream!"); | |
568 l = -1; | |
569 } else { | |
570 l = (main_info->stream.samples / main_info->stream.samplerate) * 1000; | |
571 } | |
572 | |
573 /* | |
574 * Grab the play loop mutex | |
575 */ | |
576 _DEBUG("Waiting for play loop mutex..."); | |
577 g_mutex_lock(flac_pl_mutex); | |
578 _DEBUG("Got play loop mutex"); | |
579 | |
580 input->playing = TRUE; | |
581 | |
582 flac_ip.set_info(get_title(input->filename, main_info), l, -1, main_info->stream.samplerate, main_info->stream.channels); | |
583 | |
584 thread = g_thread_create(flac_play_loop, input, TRUE, NULL); | |
585 | |
586 _LEAVE; | |
587 } | |
588 | |
589 /* --- */ | |
590 | |
591 void flac_stop(InputPlayback* input) { | |
592 | |
593 _ENTER; | |
594 | |
595 input->playing = FALSE; | |
596 | |
597 _LEAVE; | |
598 } | |
599 | |
600 /* --- */ | |
601 | |
602 void flac_pause(InputPlayback* input, gshort p) { | |
603 | |
604 _ENTER; | |
605 | |
606 input->output->pause(p); | |
607 | |
608 _LEAVE; | |
609 } | |
610 | |
611 /* --- */ | |
612 | |
613 void flac_seek(InputPlayback* input, gint time) { | |
614 | |
615 _ENTER; | |
616 | |
617 if (!input->playing) { | |
618 _DEBUG("Can not seek while not playing"); | |
619 _LEAVE; | |
620 } | |
621 | |
622 _DEBUG("Requesting seek to %d", time); | |
623 seek_to = time; | |
624 | |
625 while (-1 != seek_to) { | |
626 xmms_usleep(10000); | |
627 } | |
628 | |
629 _LEAVE; | |
630 } | |
631 | |
632 /* --- */ | |
633 | |
634 void flac_get_song_info(gchar* filename, gchar** title, gint* length) { | |
635 | |
636 gint l; | |
637 | |
638 _ENTER; | |
639 | |
640 if (FALSE == read_metadata(filename, test_decoder, test_info)) { | |
641 _ERROR("Could not read file info!"); | |
642 *length = -1; | |
643 *title = g_strdup(""); | |
644 _LEAVE; | |
645 } | |
646 | |
647 /* | |
648 * Calculate the stream length (milliseconds) | |
649 */ | |
650 if (0 == test_info->stream.samplerate) { | |
651 _ERROR("Invalid sample rate for stream!"); | |
652 l = -1; | |
653 } else { | |
654 l = (test_info->stream.samples / test_info->stream.samplerate) * 1000; | |
655 _DEBUG("Stream length: %d seconds", l/1000); | |
656 } | |
657 | |
658 *length = l; | |
659 *title = get_title(filename, test_info); | |
660 | |
661 reset_info(test_info); | |
662 | |
663 _LEAVE; | |
664 } | |
665 | |
666 /* --- */ | |
667 | |
668 TitleInput *flac_get_song_tuple(gchar* filename) { | |
669 | |
670 gint l; | |
671 TitleInput *tuple; | |
672 | |
673 _ENTER; | |
674 | |
675 if (FALSE == read_metadata(filename, test_decoder, test_info)) { | |
676 _ERROR("Could not read metadata tuple for file <%s>", filename); | |
677 _LEAVE NULL; | |
678 } | |
679 | |
680 tuple = get_tuple(filename, test_info); | |
681 | |
682 reset_info(test_info); | |
683 | |
684 _LEAVE tuple; | |
685 } | |
686 | |
687 /* --- */ | |
688 | |
689 void flac_aboutbox(void) { | |
690 | |
691 static GtkWidget* about_window; | |
692 | |
693 if (about_window) { | |
694 gdk_window_raise(about_window->window); | |
695 } | |
696 | |
697 about_window = xmms_show_message(_("About FLAC Audio Plugin"), | |
698 ("FLAC Audio Plugin (" _VERSION ")\n\n" | |
699 "Original code by\n" | |
700 "Ralf Ertzinger <ralf@skytale.net>\n" | |
701 "\n" | |
702 "http://www.skytale.net/projects/bmp-flac2/"), | |
703 _("OK"), FALSE, NULL, NULL); | |
704 g_signal_connect(G_OBJECT(about_window), "destroy", | |
705 G_CALLBACK(gtk_widget_destroyed), &about_window); | |
706 } |