diff options
author | Takamichi Horikawa <takamichiho@gmail.com> | 2017-04-12 01:04:15 +0900 |
---|---|---|
committer | Takamichi Horikawa <takamichiho@gmail.com> | 2017-04-12 01:04:15 +0900 |
commit | ab379f2bb081f3fe2ea77ed163f755f59a49e6cf (patch) | |
tree | b06142d1b4f765e23531abc50ad229494b241a04 /gtk | |
parent | c6c96944ae1bb1d7363349ec21be9dca76ee9ec4 (diff) |
added wave output
Diffstat (limited to 'gtk')
-rw-r--r-- | gtk/Makefile.am | 7 | ||||
-rw-r--r-- | gtk/configure.ac | 2 | ||||
-rw-r--r-- | gtk/fmplayer.xpm | 26 | ||||
-rw-r--r-- | gtk/fmplayer32.xpm | 46 | ||||
-rw-r--r-- | gtk/main.c | 112 | ||||
-rw-r--r-- | gtk/toneview.c | 2 | ||||
-rw-r--r-- | gtk/wavesave.c | 256 | ||||
-rw-r--r-- | gtk/wavesave.h | 8 |
8 files changed, 372 insertions, 87 deletions
diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 3524f15..fb06655 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -22,8 +22,8 @@ FMDSP_SRC=../fmdsp/fmdsp.c \ #CFLAGS= fmplayer_CPPFLAGS=-Wall -Wextra -pedantic \ -I.. \ - $(GTK3_CFLAGS) $(PORTAUDIO_CFLAGS) -fmplayer_LDADD=$(GTK3_LIBS) $(PORTAUDIO_LIBS) + $(GTK3_CFLAGS) $(PORTAUDIO_CFLAGS) $(SNDFILE_CFLAGS) +fmplayer_LDADD=$(GTK3_LIBS) $(PORTAUDIO_LIBS) $(SNDFILE_LIBS) if ENABLE_NEON LIBOPNA_SRC+=../libopna/opnassg-sinc-neon.s @@ -42,9 +42,12 @@ endif fmplayer_SOURCES=main.c \ toneview.c \ + wavesave.c \ ../tonedata/tonedata.c \ ../common/fmplayer_file.c \ ../common/fmplayer_file_gio.c \ + ../common/fmplayer_work_opna.c \ + ../common/fmplayer_drumrom_unix.c \ $(LIBOPNA_SRC) \ $(FMDRIVER_SRC) \ $(FMDSP_SRC) diff --git a/gtk/configure.ac b/gtk/configure.ac index 79d02c1..cab8553 100644 --- a/gtk/configure.ac +++ b/gtk/configure.ac @@ -6,9 +6,9 @@ AC_PROG_RANLIB AM_PROG_AR AM_PROG_AS -dnl AM_PATH_SDL2([2.0.5]) PKG_CHECK_MODULES([PORTAUDIO], [portaudio-2.0]) PKG_CHECK_MODULES([GTK3], [gtk+-3.0 cairo]) +PKG_CHECK_MODULES([SNDFILE], [sndfile]) AC_ARG_ENABLE([neon], AS_HELP_STRING([--enable-neon], [Enable NEON optimized functions for SSG sinc filtering and fmdsp palette lookup. Tested on Cortex-A53 (Raspberry PI 3)])) AM_CONDITIONAL([ENABLE_NEON], [test "x$enable_neon" = "xyes"]) diff --git a/gtk/fmplayer.xpm b/gtk/fmplayer.xpm new file mode 100644 index 0000000..d52d9f4 --- /dev/null +++ b/gtk/fmplayer.xpm @@ -0,0 +1,26 @@ +/* XPM */ +static char *fmplayer_xpm_16[] = { +/* columns rows colors chars-per-pixel */ +"16 16 4 1 ", +" c #40A040", +". c #C0C0C0", +"X c gray25", +"o c white", +/* pixels */ +" ", +" .. .. .. .. .. ", +" .. .. .. .. .. ", +"XXXXXXXXXXXXXXXX", +"oXoXoXoXXXXXXXXX", +"oXoXoooXXXXXXXXX", +"XoXXoXoXXXXXXXXX", +"XoXXoXoXXXXXXXXX", +"XoXXoXoXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XoXXXooXXoXXXoXX", +"oXoXoXXXoXoXoXoX", +"XXoXooXXoXoXXoXX", +"XoXXoXoXoXoXoXoX", +"oooXXoXXXoXXXoXX", +"XXXXXXXXXXXXXXXX" +}; diff --git a/gtk/fmplayer32.xpm b/gtk/fmplayer32.xpm new file mode 100644 index 0000000..062d12e --- /dev/null +++ b/gtk/fmplayer32.xpm @@ -0,0 +1,46 @@ +/* XPM */ +static char *fmplayer_xpm_32[] = { +/* columns rows colors chars-per-pixel */ +"32 32 8 1 ", +" c gray25", +". c #606060", +"X c #40A040", +"o c #808080", +"O c #A0A0A0", +"+ c #C0C0C0", +"@ c #D8D8D8", +"# c white", +/* pixels */ +"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", +"OOXOOOXOOOXOOOXOOOXOOOXOOOXOOOXO", +"##X###X###X###X###X###X###X###X#", +"@@X@@@X@@@X@@@X@@@X@@@X@@@X@@@X@", +"++X+++X+++X+++X+++X+++X+++X+++X+", +"................................", +" ", +" ", +" # # # # ", +" # # ## ## ", +" # # # # # ", +" # # # ", +" # # # ", +" # # # ", +" # # # ", +" # # # ", +" ", +" ### ### ### ### ", +" # # # # # # # # ", +" # # # # # # ", +" # #### # # ### ", +" # # # # # # # ", +" # # # # # # # ", +" # # # # # # # ", +" ##### ### ### ### ", +" ", +" ", +"++X+++X+++X+++X+++X+++X+++X+++X+", +"++X+++X+++X+++X+++X+++X+++X+++X+", +"OOXOOOXOOOXOOOXOOOXOOOXOOOXOOOXO", +"ooXoooXoooXoooXoooXoooXoooXoooXo", +"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" +}; @@ -17,6 +17,11 @@ #include "toneview.h" #include "oscillo/oscillo.h" #include "oscilloview.h" +#include "wavesave.h" +#include "common/fmplayer_common.h" + +#include "fmplayer.xpm" +#include "fmplayer32.xpm" #define DATADIR "/.local/share/fmplayer/" //#define FMDSP_2X @@ -42,8 +47,6 @@ static struct { struct ppz8 ppz8; struct fmdriver_work work; struct fmdsp fmdsp; - char drum_rom[OPNA_ROM_SIZE]; - bool drum_rom_loaded; char adpcm_ram[OPNA_ADPCM_RAM_SIZE]; struct fmplayer_file *fmfile; void *data; @@ -80,6 +83,16 @@ static void on_menu_quit(GtkMenuItem *menuitem, gpointer ptr) { quit(); } +static void on_menu_save(GtkMenuItem *menuitem, gpointer ptr) { + (void)menuitem; + (void)ptr; + if (g.current_uri) { + char *uri = g_strdup(g.current_uri); + wavesave_dialog(GTK_WINDOW(g.mainwin), uri); + g_free(uri); + } +} + static void on_tone_view(GtkMenuItem *menuitem, gpointer ptr) { (void)menuitem; (void)ptr; @@ -130,67 +143,6 @@ static int pastream_cb(const void *inptr, void *outptr, unsigned long frames, return paContinue; } -static void opna_int_cb(void *userptr) { - struct fmdriver_work *work = (struct fmdriver_work *)userptr; - work->driver_opna_interrupt(work); -} - -static void opna_mix_cb(void *userptr, int16_t *buf, unsigned samples) { - struct ppz8 *ppz8 = (struct ppz8 *)userptr; - ppz8_mix(ppz8, buf, samples); -} - -static void opna_writereg_libopna(struct fmdriver_work *work, unsigned addr, unsigned data) { - struct opna_timer *timer = (struct opna_timer *)work->opna; - opna_timer_writereg(timer, addr, data); -} - -static unsigned opna_readreg_libopna(struct fmdriver_work *work, unsigned addr) { - (void)work; - //struct opna_timer *timer = (struct opna_timer *)work->opna; - return opna_readreg(&g.opna, addr); -} - -static uint8_t opna_status_libopna(struct fmdriver_work *work, bool a1) { - struct opna_timer *timer = (struct opna_timer *)work->opna; - uint8_t status = opna_timer_status(timer); - if (!a1) { - status &= 0x83; - } - return status; -} - -static void load_drumrom(void) { - const char *path = "ym2608_adpcm_rom.bin"; - const char *home = getenv("HOME"); - char *dpath = 0; - if (home) { - const char *datadir = DATADIR; - dpath = malloc(strlen(home)+strlen(datadir)+strlen(path) + 1); - if (dpath) { - strcpy(dpath, home); - strcat(dpath, datadir); - strcat(dpath, path); - path = dpath; - } - } - FILE *rhythm = fopen(path, "r"); - free(dpath); - if (!rhythm) goto err; - if (fseek(rhythm, 0, SEEK_END) != 0) goto err_file; - long size = ftell(rhythm); - if (size != OPNA_ROM_SIZE) goto err_file; - if (fseek(rhythm, 0, SEEK_SET) != 0) goto err_file; - if (fread(g.drum_rom, 1, OPNA_ROM_SIZE, rhythm) != OPNA_ROM_SIZE) goto err_file; - fclose(rhythm); - g.drum_rom_loaded = true; - return; -err_file: - fclose(rhythm); -err: - return; -} - static void load_fontrom(void) { const char *path = "font.rom"; const char *home = getenv("HOME"); @@ -261,24 +213,10 @@ static bool openfile(const char *uri) { fmplayer_file_free(g.fmfile); g.fmfile = fmfile; unsigned mask = opna_get_mask(&g.opna); - opna_reset(&g.opna); + g.work = (struct fmdriver_work){0}; + memset(g.adpcm_ram, 0, sizeof(g.adpcm_ram)); + fmplayer_init_work_opna(&g.work, &g.ppz8, &g.opna, &g.opna_timer, g.adpcm_ram); opna_set_mask(&g.opna, mask); - if (!g.drum_rom_loaded) { - load_drumrom(); - } - if (g.drum_rom_loaded) { - opna_drum_set_rom(&g.opna.drum, g.drum_rom); - } - opna_adpcm_set_ram_256k(&g.opna.adpcm, g.adpcm_ram); - ppz8_init(&g.ppz8, SRATE, PPZ8MIX); - opna_timer_reset(&g.opna_timer, &g.opna); - memset(&g.work, 0, sizeof(g.work)); - g.work.opna_writereg = opna_writereg_libopna; - g.work.opna_readreg = opna_readreg_libopna; - g.work.opna_status = opna_status_libopna; - g.work.opna = &g.opna_timer; - g.work.ppz8 = &g.ppz8; - g.work.ppz8_functbl = &ppz8_functbl; char *disppath = g_filename_from_uri(uri, 0, 0); if (disppath) { strncpy(g.work.filename, disppath, sizeof(g.work.filename)-1); @@ -286,9 +224,7 @@ static bool openfile(const char *uri) { } else { strncpy(g.work.filename, uri, sizeof(g.work.filename)-1); } - opna_timer_set_int_callback(&g.opna_timer, opna_int_cb, &g.work); - opna_timer_set_mix_callback(&g.opna_timer, opna_mix_cb, &g.ppz8); - fmplayer_file_load(&g.work, g.fmfile); + fmplayer_file_load(&g.work, g.fmfile, 1); fmdsp_vram_init(&g.fmdsp, &g.work, g.vram); Pa_StartStream(g.pastream); g.pa_paused = false; @@ -321,6 +257,9 @@ static GtkWidget *create_menubar() { GtkWidget *open = gtk_menu_item_new_with_label("Open"); //g_signal_connect(open, "activate", G_CALLBACK(on_menu_open), 0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), open); + GtkWidget *save = gtk_menu_item_new_with_label("Save wavefile"); + g_signal_connect(save, "activate", G_CALLBACK(on_menu_save), 0); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), save); GtkWidget *quit = gtk_menu_item_new_with_label("Quit"); g_signal_connect(quit, "activate", G_CALLBACK(on_menu_quit), 0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), quit); @@ -544,6 +483,13 @@ int main(int argc, char **argv) { #endif load_fontrom(); gtk_init(&argc, &argv); + { + GList *iconlist = 0; + iconlist = g_list_append(iconlist, gdk_pixbuf_new_from_xpm_data(fmplayer_xpm_16)); + iconlist = g_list_append(iconlist, gdk_pixbuf_new_from_xpm_data(fmplayer_xpm_32)); + gtk_window_set_default_icon_list(iconlist); + g_list_free(iconlist); + } GtkWidget *w = gtk_window_new(GTK_WINDOW_TOPLEVEL); g.mainwin = w; gtk_window_set_resizable(GTK_WINDOW(w), FALSE); diff --git a/gtk/toneview.c b/gtk/toneview.c index 614130b..64c4e98 100644 --- a/gtk/toneview.c +++ b/gtk/toneview.c @@ -42,7 +42,7 @@ gboolean tick_cb(GtkWidget *widget, GdkFrameClock *clock, gpointer ptr) { tonedata_ch_normalize_tl(&g.tonedata_n.ch[c]); } if (g.format != g.format_disp || - fmplayer_tonedata_channel_isequal(&g.tonedata_n.ch[c], &g.tonedata_n_disp.ch[c])) { + !fmplayer_tonedata_channel_isequal(&g.tonedata_n.ch[c], &g.tonedata_n_disp.ch[c])) { g.tonedata_n_disp.ch[c] = g.tonedata_n.ch[c]; tonedata_ch_string(g.format, g.strbuf, &g.tonedata_n.ch[c], 0); gtk_label_set_text(GTK_LABEL(g.label[c]), g.strbuf); diff --git a/gtk/wavesave.c b/gtk/wavesave.c new file mode 100644 index 0000000..0af656f --- /dev/null +++ b/gtk/wavesave.c @@ -0,0 +1,256 @@ +#include "wavesave.h" +#include "common/fmplayer_file.h" +#include <sndfile.h> +#include <string.h> +#include "libopna/opna.h" +#include "libopna/opnatimer.h" +#include "common/fmplayer_common.h" + +enum { + SRATE = 55467, +}; + +static void msgbox_err(GtkWindow *parent, const char *msg) { + GtkWidget *d = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + msg); + gtk_dialog_run(GTK_DIALOG(d)); + gtk_widget_destroy(d); +} + +static sf_count_t gio_get_filelen(void *ptr) { + (void)ptr; + return -1; +} + +static sf_count_t gio_seek(sf_count_t offset, int whence, void *ptr) { + GFileOutputStream *os = ptr; + GSeekType st; + switch (whence) { + default: + st = G_SEEK_CUR; + break; + case SEEK_SET: + st = G_SEEK_SET; + break; + case SEEK_END: + st = G_SEEK_END; + break; + } + if (!g_seekable_seek(G_SEEKABLE(os), offset, st, 0, 0)) return -1; + return g_seekable_tell(G_SEEKABLE(os)); +} + +static sf_count_t gio_read(void *buf, sf_count_t count, void *ptr) { + (void)buf; + (void)count; + (void)ptr; + return -1; +} + +static sf_count_t gio_write(const void *buf, sf_count_t count, void *ptr) { + GFileOutputStream *os = ptr; + return g_output_stream_write(G_OUTPUT_STREAM(os), buf, count, 0, 0); +} + +static sf_count_t gio_tell(void *ptr) { + GFileOutputStream *os = ptr; + return g_seekable_tell(G_SEEKABLE(os)); +} + +static SF_VIRTUAL_IO sf_virt_gio = { + .get_filelen = gio_get_filelen, + .seek = gio_seek, + .read = gio_read, + .write = gio_write, + .tell = gio_tell, +}; + +struct fadeout { + struct opna_timer *timer; + struct fmdriver_work *work; + uint64_t vol; + uint8_t loopcnt; +}; + +static bool fadeout_mix( + struct fadeout *fadeout, + int16_t *buf, unsigned frames +) { + opna_timer_mix(fadeout->timer, buf, frames); + for (unsigned i = 0; i < frames; i++) { + int vol = fadeout->vol >> 16; + buf[i*2+0] = (buf[i*2+0] * vol) >> 16; + buf[i*2+1] = (buf[i*2+1] * vol) >> 16; + if (fadeout->work->loop_cnt >= fadeout->loopcnt) { + fadeout->vol = (fadeout->vol * 0xffff0000ull) >> 32; + } + } + return fadeout->vol; +} + +struct thread_write_data { + struct opna opna; + struct opna_timer timer; + struct ppz8 ppz8; + struct fmdriver_work work; + struct fadeout fadeout; + SNDFILE *sndfile; + GtkDialog *dialog; + GtkProgressBar *pbar; + double fraction; + uint8_t adpcm_ram[OPNA_ADPCM_RAM_SIZE]; +}; + +static gboolean idle_close_dialog(gpointer ptr) { + struct thread_write_data *data = ptr; + gtk_dialog_response(data->dialog, GTK_RESPONSE_ACCEPT); + return G_SOURCE_REMOVE; +} + +static gboolean idle_progress_fraction(gpointer ptr) { + struct thread_write_data *data = ptr; + gtk_progress_bar_set_fraction(data->pbar, data->fraction); + return G_SOURCE_REMOVE; +} + +static gpointer thread_write(gpointer ptr) { + struct thread_write_data *data = ptr; + enum { + BUFLEN = 1024, + }; + int16_t buf[BUFLEN*2]; + for (;;) { + memset(buf, 0, sizeof(buf)); + bool end = !fadeout_mix(&data->fadeout, buf, BUFLEN); + if (sf_writef_short(data->sndfile, buf, BUFLEN) != BUFLEN) { + return 0; + } + double newfrac = (double)data->work.timerb_cnt / data->work.loop_timerb_cnt; + if ((newfrac - data->fraction) > 0.005) { + data->fraction = newfrac; + g_idle_add(idle_progress_fraction, ptr); + } + if (end) break; + } + g_idle_add(idle_close_dialog, ptr); + return 0; +} + +static void wavesave(GtkWindow *parent, + struct fmplayer_file *fmfile, + const char *saveuri, + bool flac, + int loopcnt) { + if (loopcnt < 1) loopcnt = 1; + if (loopcnt > 0xff) loopcnt = 0xff; + GFile *savefile = g_file_new_for_uri(saveuri); + GFileOutputStream *os = g_file_replace(savefile, 0, TRUE, 0, 0, 0); + if (!os) { + msgbox_err(parent, "Cannot open output file"); + g_object_unref(savefile); + return; + } + SF_INFO sfinfo = { + .samplerate = SRATE, + .channels = 2, + .format = (flac ? SF_FORMAT_FLAC : SF_FORMAT_WAV | SF_ENDIAN_CPU) | SF_FORMAT_PCM_16, + .seekable = 1, + }; + SNDFILE *sndfile = sf_open_virtual(&sf_virt_gio, SFM_WRITE, &sfinfo, os); + if (!sndfile) { + char *msg = g_strjoin(0, "SNDFILE Error: ", sf_strerror(sndfile), (char *)0); + msgbox_err(parent, msg); + g_free(msg); + g_object_unref(os); + g_object_unref(savefile); + return; + } + struct thread_write_data *tdata = g_new0(struct thread_write_data, 1); + fmplayer_init_work_opna(&tdata->work, &tdata->ppz8, &tdata->opna, &tdata->timer, tdata->adpcm_ram); + fmplayer_file_load(&tdata->work, fmfile, loopcnt); + tdata->fadeout.timer = &tdata->timer; + tdata->fadeout.work = &tdata->work; + tdata->fadeout.vol = 1ull<<32; + tdata->fadeout.loopcnt = loopcnt; + GtkWidget *dialog = gtk_dialog_new_with_buttons("Writing...", + parent, + GTK_DIALOG_MODAL, + "Cancel", + GTK_RESPONSE_CANCEL, + (void*)0); + GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + gtk_container_add(GTK_CONTAINER(content), gtk_label_new("Writing...")); + GtkWidget *progress = gtk_progress_bar_new(); + gtk_container_add(GTK_CONTAINER(content), progress); + tdata->sndfile = sndfile; + tdata->dialog = GTK_DIALOG(dialog); + tdata->pbar = GTK_PROGRESS_BAR(progress); + tdata->fraction = 0.0; + GThread *thread = g_thread_new("Write Worker", thread_write, tdata); + gtk_widget_show_all(dialog); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + g_thread_join(thread); + sf_close(sndfile); + g_free(tdata); + g_object_unref(os); + g_object_unref(savefile); +} + +enum { + RESPONSE_IGNORE = 1, +}; + +void wavesave_dialog(GtkWindow *parent, const char *uri) { + GtkWidget *fcd = gtk_file_chooser_dialog_new("Save wave file", + parent, + GTK_FILE_CHOOSER_ACTION_SAVE, + "Cancel", + GTK_RESPONSE_CANCEL, + "Save", + GTK_RESPONSE_ACCEPT, + (void *)0 + ); + GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + GtkWidget *typecombo = gtk_combo_box_text_new(); + gtk_widget_set_halign(typecombo, GTK_ALIGN_END); + gtk_box_pack_start(GTK_BOX(hbox), typecombo, FALSE, FALSE, 5); + gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(typecombo), 0, "RIFF WAVE (*.wav)"); + gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(typecombo), 0, "FLAC (*.flac)"); + gtk_combo_box_set_active(GTK_COMBO_BOX(typecombo), 0); + GtkWidget *loopbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + gtk_widget_set_halign(loopbox, GTK_ALIGN_END); + gtk_box_pack_start(GTK_BOX(hbox), loopbox, FALSE, FALSE, 5); + gtk_box_pack_start(GTK_BOX(loopbox), gtk_label_new("Loop:"), FALSE, FALSE, 5); + GtkWidget *loopspin = gtk_spin_button_new_with_range(1.0, 255.0, 1.0); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(loopspin), 2.0); + gtk_box_pack_start(GTK_BOX(loopbox), loopspin, FALSE, FALSE, 5); + gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(fcd))), + hbox); + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(fcd), TRUE); + + char *saveuri = 0; + gtk_widget_show_all(fcd); + gint res; + while ((res = gtk_dialog_run(GTK_DIALOG(fcd))) == RESPONSE_IGNORE); + if (res == GTK_RESPONSE_ACCEPT) { + saveuri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(fcd)); + } + int loopcnt = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(loopspin)); + bool flac = gtk_combo_box_get_active(GTK_COMBO_BOX(typecombo)); + gtk_widget_destroy(fcd); + if (saveuri) { + enum fmplayer_file_error error; + struct fmplayer_file *fmfile = fmplayer_file_alloc(uri, &error); + if (!fmfile) { + char *msg = g_strjoin(0, "Cannot load file: ", fmplayer_file_strerror(error), (char *)0); + msgbox_err(parent, msg); + g_free(msg); + } else { + wavesave(parent, fmfile, saveuri, flac, loopcnt); + fmplayer_file_free(fmfile); + } + } + g_free(saveuri); +} diff --git a/gtk/wavesave.h b/gtk/wavesave.h new file mode 100644 index 0000000..7dcea1f --- /dev/null +++ b/gtk/wavesave.h @@ -0,0 +1,8 @@ +#ifndef MYON_FMPLAYER_GTK_WAVESAVE_H_INCLUDED +#define MYON_FMPLAYER_GTK_WAVESAVE_H_INCLUDED + +#include <gtk/gtk.h> + +void wavesave_dialog(GtkWindow *parent, const char *uri); + +#endif // MYON_FMPLAYER_GTK_WAVESAVE_H_INCLUDED |