'OHRRPGCE - music and sound effects editors & importing
'(C) Copyright 2017 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.

#include "config.bi"
#include "allmodex.bi"
#include "common.bi"
#include "customsubs.bi"
#include "cglobals.bi"
#include "custom.bi"
#include "const.bi"
#include "uiconst.bi"
#include "loading.bi"
#include "thingbrowser.bi"
#include "music.bi"
#include "gfx.bi"


'''' Local functions

DECLARE FUNCTION mp3_to_ogg (in_file as string, out_file as string, quality as integer = 4, filter_high_freq as bool = YES) as string
DECLARE FUNCTION mp3_to_wav (in_file as string, out_file as string) as string
DECLARE FUNCTION wav_to_ogg (in_file as string, out_file as string, quality as integer = 4, filter_high_freq as bool = YES, comments() as string) as string

DECLARE SUB export_songlist()

DECLARE SUB delete_song (byval songnum as integer, songfile as string, bamfile as string)
DECLARE SUB importsong_save_song_data(songname as string, byval songnum as integer)
DECLARE SUB importsong_exportsong(songfile as string, bamfile as string, file_ext as string, songname as string)
DECLARE SUB importsong_get_song_info (songname as string, songfile as string, bamfile as string, byval songnum as integer, file_ext as string, menu() as string, metadata as string, byref songlength as double, selectable() as bool, state as MenuState)
DECLARE SUB importsong_import_song_file (songname as string, songfile as string, bamfile as string, byval songnum as integer)
DECLARE SUB importsfx_get_sfx_info(sfxname as string, sfxfile as string, byval sfxnum as integer, file_ext as string, menu() as string, metadata as string, selectable() as bool, state as MenuState)
DECLARE SUB importsfx_save_sfx_data(sfxname as string, byval sfxnum as integer)
DECLARE SUB importsfx_exportsfx(sfxfile as string, file_ext as string, sfxname as string)
DECLARE SUB importsfx_importsfxfile(sfxname as string, sfxfile as string, byval sfxnum as integer, file_ext as string)


'==========================================================================================
'                                   Audio re-encoding
'==========================================================================================


FUNCTION find_madplay () as string
 STATIC cached as bool = NO
 STATIC cached_app as string
 IF cached THEN RETURN cached_app
 cached_app = find_helper_app("madplay", YES)
 cached = YES
 RETURN cached_app
END FUNCTION

FUNCTION find_oggenc () as string
 STATIC cached as bool = NO
 STATIC cached_app as string
 IF cached THEN RETURN cached_app
 cached_app = find_helper_app("oggenc", YES)
 IF cached_app = "" THEN cached_app = find_helper_app("oggenc2")
 cached = YES
 RETURN cached_app
END FUNCTION

'Read ID3 tags from an mp3 and return them as a list of strings in the form "key=value"
'NOTE: This invokes madplay to get the ID3 tags, but we already have
'read_mp3_metadata(), which could be extended to the do the same without madplay.
'However, madplay does translated various ID codes like "TPUB" to "Publisher" for us.
SUB mp3_ID3_tags (in_file as string, comments() as string)
 ERASE comments

 DIM as string app, args
 app = find_madplay()
 'Shouldn't happen
 IF app = "" THEN
  visible_debug "Can not read MP3 files: " + missing_helper_message("madplay" + DOTEXE)
  EXIT SUB
 END IF

 'We throw away encoder comments (-v)
 args = " -T -q " & escape_filename(in_file)

 DIM errcode as integer
 DIM as string stdout_s, stderr_s
 errcode = run_and_get_output(escape_filename(app) & args, stdout_s, stderr_s)
 IF errcode THEN
  visible_debug "Couldn't get ID3 tags from MP3 file: " & stderr_s
  EXIT SUB
 END IF

 'madplay actually prints all the tag info to stderr...
 DIM lines() as string
 split stderr_s, lines()

 'Each line is of the form "           Year: 2009"
 'but might be word-wrapped.
 'Also the "Copyright (C)" and "Produced (P)" lines are
 'are exceptions for some reason (they have no 'Key:' part), and it looks like there
 'can be other tags with no 'key' too.
 FOR idx as integer = 0 TO UBOUND(lines)
  DIM tag as string = TRIM(lines(idx))
  IF LEN(tag) = 0 THEN CONTINUE FOR
  DIM colon as integer = INSTR(tag, ": ")
  DIM as string key, text

  'Copyright and Produced special cases (start with 17 spaces)
  IF INSTR(tag, "Copyright ") = 1 THEN
   key = "Copyright"
   text = MID(tag, 11)
  ELSEIF INSTR(tag, "Produced ") = 1 THEN
   key = "Produced"
   text = MID(tag, 10)
  ELSEIF colon = 0 THEN
   'Other lines are generally garbage, a repeat of the previous line
   CONTINUE FOR
  ELSE
   key = LEFT(tag, colon - 1)
   text = MID(tag, colon + 2)
  END IF

  'Concatenate apparent wrapped lines
  WHILE LEN(lines(idx)) > 65 ANDALSO idx + 1 <= UBOUND(lines) ANDALSO LEFT(lines(idx + 1), 17) = SPACE(17)
   idx += 1
   text += " " + TRIM(lines(idx))
  WEND

  IF key = "Encoder" THEN CONTINUE FOR  'We're reencoding, ditch that

  a_append comments(), key & "=" & text
 NEXT
END SUB

'Returns error message, or "" on success
FUNCTION mp3_to_ogg (in_file as string, out_file as string, quality as integer = 4, filter_high_freq as bool = YES) as string
 DIM as string tempwav
 DIM as string ret
 tempwav = tmpdir & "temp." & randint(100000) & ".wav"
 ret = mp3_to_wav(in_file, tempwav)
 IF LEN(ret) THEN RETURN ret
 DIM comments() as string
 mp3_ID3_tags in_file, comments()
 ret = wav_to_ogg(tempwav, out_file, quality, filter_high_freq, comments())
 safekill tempwav
 RETURN ret
END FUNCTION

'Returns error message, or "" on success
FUNCTION mp3_to_wav (in_file as string, out_file as string) as string
 DIM as string app, args, ret
 IF NOT isfile(in_file) THEN RETURN "mp3 to wav conversion: " & in_file & " does not exist"
 app = find_madplay()
 IF app = "" THEN RETURN "Can not read MP3 files: " + missing_helper_message("madplay" + DOTEXE)

 args = " -o wave:" & escape_filename(out_file) & " " & escape_filename(in_file)
 ret = spawn_and_wait(app, args)
 IF LEN(ret) THEN
  safekill out_file
  RETURN ret
 END IF

 IF NOT isfile(out_file) THEN RETURN "Could not find " + out_file + ": " + app + " must have failed"
 RETURN ""
END FUNCTION

'Returns error message, or "" on success
FUNCTION wav_to_ogg (in_file as string, out_file as string, quality as integer = 4, filter_high_freq as bool = YES, comments() as string) as string
 DIM as string app, args, ret
 IF NOT isfile(in_file) THEN RETURN "wav to ogg conversion: " & in_file & " does not exist"
 app = find_oggenc()
 IF app = "" THEN RETURN "Can not convert to OGG: " + missing_helper_message("oggenc" DOTEXE " and oggenc2" DOTEXE)

 args = " -q " & quality & " -o " & escape_filename(out_file) & " " & escape_filename(in_file)
 IF filter_high_freq = NO THEN
  args &= " --advanced-encode-option lowpass_frequency=22"
 END IF
 FOR idx as integer = 0 TO UBOUND(comments)
  args &= " -c " & escape_filename(comments(idx))
 NEXT
 ret = spawn_and_wait(app, args)
 IF LEN(ret) THEN
  safekill out_file
  RETURN "wav to ogg conversion failed: " & ret
 END IF

 IF NOT isfile(out_file) THEN RETURN "Could not find " + out_file + ": " + app + " must have failed"
 RETURN ""
END FUNCTION


'==========================================================================================
'                                Audio re-encoding menus
'==========================================================================================


'Returns true and sets 'quality' if not cancelled
FUNCTION pick_ogg_quality(byref quality as integer, byref filter_high_freq as bool) as bool
 filter_high_freq = YES
 STATIC q as integer = 2
 DIM i as integer
 DIM descrip as string
 setkeys
 DO
  setwait 55
  setkeys
  IF keyval(ccCancel) > 1 THEN
   RETURN NO  'cancelled
  END IF
  IF keyval(scF1) > 1 THEN show_help "pick_ogg_quality"
  IF keyval(scTab) > 1 THEN
   filter_high_freq = yesno("Filter out high-pitched noise? YES is default, NO increases file size " _
                            "without improving audio quality, but suitable for hissing noise sfx", YES, YES)
  END IF
  IF enter_or_space() THEN EXIT DO
  CONST MINQ = -1
  CONST MAXQ = 9
  intgrabber q, MINQ, MAXQ
  clearpage vpage
  centerbox rCenter, 105, 300, 54, 4, vpage
  'We don't know the number of channels, so assume 2...
  edgeprint "Pick Ogg quality level: " & q & " (~" & oggenc_quality_levels(2, q) & "kbps)", pCentered, 86, uilook(uiText), vpage
  CONST thipsize = 23
  FOR i = 0 TO q + 1
   rectangle rCenter - ((MAXQ - MINQ + 1) * thipsize) \ 2 + thipsize * i, 100, thipsize - 1, 16, uilook(uiText), vpage
  NEXT i
  SELECT CASE q
   CASE -1: descrip = "scratchy, smallest"
   CASE 0: descrip = "not too bad, very small"
   CASE 1: descrip = "pretty good, quite small"
   CASE 2: descrip = "good, pretty small"
   CASE 3: descrip = "good, smallish"
   CASE 4: descrip = "good, medium sized"
   CASE 5: descrip = "good, biggish"
   CASE 6: descrip = "better than you need, big"
   CASE 7: descrip = "much better than you need, too big"
   CASE 8: descrip = "excessive, wasteful"
   CASE 9: descrip = "very excessive, very wasteful"
   CASE 10: descrip = "flagrantly excessive and wasteful"
  END SELECT
  edgeprint descrip, pCentered, 118, uilook(uiText), vpage
  edgeprint "TAB: advanced options", pLeft, pBottom, uilook(uiMenuItem), vpage
  setvispage vpage
  dowait
 LOOP
 quality = q
 RETURN YES
END FUNCTION

'Convert an MP3 to an OGG.
'Sets 'mp3' and 'oggtemp' to the path to the new file (indicating oggtemp is a temp file to be deleted)
SUB import_convert_mp3(byref mp3 as string, byref oggtemp as string)
#IFDEF MINIMAL_OS
 notification !"Warning:\n" "Can't convert .mp3 files to .ogg on this platform, so the file will just be imported as an .mp3. It might not play in all builds of the OHRRPGCE, such as web ports."
#ELSE
 DIM ogg_quality as integer
 DIM filter_high_freq as bool
 IF pick_ogg_quality(ogg_quality, filter_high_freq) = NO THEN mp3 = "" : EXIT SUB
 oggtemp = tmpdir & "temp." & randint(100000) & ".ogg"
 clearpage vpage
 basic_textbox "Please wait, converting to OGG...", uilook(uiText), vpage
 setvispage vpage, NO
 DIM ret as string = mp3_to_ogg(mp3, oggtemp, ogg_quality, filter_high_freq)
 IF LEN(ret) THEN
  visible_debug ret
  mp3 = ""
  EXIT SUB
 END IF
 IF NOT isfile(oggtemp) THEN
  'This branch ought to be unreachable...
  visible_debug "MP3 conversion failed."
  mp3 = ""
  EXIT SUB
 END IF
 mp3 = oggtemp
#ENDIF
END SUB

'Convert a WAV to an OGG.
'Sets 'wav' and 'oggtemp' to the path to the new file (indicating oggtemp is a temp file to be deleted)
SUB import_convert_wav(byref wav as string, byref oggtemp as string)
#IFDEF MINIMAL_OS
 notification "Can't encode .wav files to .ogg on this platform. Please encode it manually."
 wav = ""
#ELSE

 DIM ogg_quality as integer
 DIM filter_high_freq as bool
 IF pick_ogg_quality(ogg_quality, filter_high_freq) = NO THEN wav = "" : EXIT SUB
 oggtemp = tmpdir & "temp." & randint(100000) & ".ogg"
 clearpage vpage
 basic_textbox "Please wait, converting to OGG...", uilook(uiText), vpage
 setvispage vpage, NO
 'WAV files CAN contain metadata and even ID3 tags, but we don't support them yet
 DIM comments() as string
 DIM ret as string = wav_to_ogg(wav, oggtemp, ogg_quality, filter_high_freq, comments())
 IF LEN(ret) THEN
  visible_debug ret
  wav = ""
  EXIT SUB
 END IF
 IF NOT isfile(oggtemp) THEN
  'This branch ought to be unreachable...
  visible_debug "WAV conversion failed."
  wav = ""
  EXIT SUB
 END IF
 wav = oggtemp
#ENDIF
END SUB


'==========================================================================================
'                                  Music Import/Editor
'==========================================================================================

SUB song_editor_main ()
 IF read_config_bool("thingbrowser.enable_top_level", YES) THEN
  DIM b as SongBrowser
  b.browse(-1, , @importsong)
 ELSE
  importsong 0
 END IF
 music_stop
END SUB

FUNCTION song_picker (recindex as integer = -1) as integer
 DIM b as SongBrowser
 RETURN b.browse(recindex, , @importsong, NO)
END FUNCTION

FUNCTION song_picker_or_none (recindex as integer = -1) as integer
 DIM b as SongBrowser
 RETURN b.browse(recindex - 1, YES , @importsong, NO) + 1
END FUNCTION

SUB importsong_draw_seek_bar(seekbar as RectType, seekpos as double, songlength as double, page as integer)
 WITH seekbar
  rectangle .x, .y, .w, .h, findrgb(0,50,50), page
  DIM songpos as double = music_gettime()
  CONST border = 2
  IF songpos >= 0 ANDALSO songlength > 0 THEN
   DIM barlen as integer = .w * songpos / songlength
   rectangle .x, .y, barlen, .h, findrgb(50,50,220), page
   rectangle .x + barlen, .y, 1, .h, findrgb(200,200,255), page
  END IF
  drawbox .x - border, .y - border, .w + border*2, .h + border*2, findrgb(200,200,200), border, page
  DIM text as string
  IF seekpos >= 0.0 THEN  'Hovering
   text = format_duration(seekpos, 2, YES)
  ELSEIF songpos >= 0 THEN
   text = format_duration(songpos, 2, YES)
  ELSE
   text = "???"
  END IF
  IF songlength >= 0 THEN
   text &= "/" & format_duration(songlength, 2, YES)
  ELSE
   text &= "/???"
  END IF
  IF music_seekable = NO THEN text &= " (Not seekable)"
  edgeprint text, pCentered, .y + 2, uilook(uiMenuItem), page
 END WITH
END SUB

FUNCTION importsong (byval songnum as integer) as integer
'songnum is the song to start with, or > max to add a new one.
'Return value is the last selected song

IF songnum > gen(genMaxSong) THEN
 'Requested a new one
 gen(genMaxSong) += 1
 songnum = gen(genMaxSong)
 importsong_save_song_data "", songnum
END IF

REDIM menu(6) as string
REDIM selectable(6) as bool
menu(0) = "Previous Menu"
menu(3) = "Import Song..."
menu(4) = "Export Song..."
menu(5) = "Delete Song"

DIM state as MenuState
state.size = 24
state.autosize = YES
state.autosize_ignore_lines = 1
state.pt = 1

DIM songname as string = ""
DIM songfile as string = ""
DIM songlength as double

DIM seekbar as RectType
seekbar.x = 20
'seekbar.y = pBottom - 15   'Continually updated
'seekbar.w = vpages(vpage)->size.w - 40
seekbar.h = 14


DIM bamfile as string = ""  '"" if none, differs from songfile if it's a BAM fallback
DIM file_ext as string
DIM metadata as string
'Also plays the song
importsong_get_song_info songname, songfile, bamfile, songnum, file_ext, menu(), metadata, songlength, selectable(), state

setkeys YES
DO
 setwait 55
 setkeys YES
 IF keyval(ccCancel) > 1 THEN EXIT DO
 IF keyval(scF1) > 1 THEN show_help "import_songs"
 IF keyval(scF2) > 1 THEN Custom_volume_menu
 IF keyval(scF3) > 1 THEN music_backend_menu
 IF keyval(scF4) > 1 THEN export_songlist

 usemenu state, selectable()

 IF state.pt = 2 AND songfile <> "" THEN
  strgrabber songname, 30
  menu(2) = "Name: " + songname
 ELSE
  '-- check for switching song
  DIM newsong as integer = songnum
  IF intgrabber(newsong, 0, gen(genMaxSong), scLeftCaret, scRightCaret) THEN
   importsong_save_song_data songname, songnum
   songnum = newsong
   state.need_update = YES
  END IF
  IF keyval(ccLeft) > 1 AND songnum > 0 THEN
   importsong_save_song_data songname, songnum
   songnum -= 1
   state.need_update = YES
  END IF
  IF keyval(ccRight) > 1 AND songnum < 32767 THEN
   importsong_save_song_data songname, songnum
   songnum += 1
   IF needaddset(songnum, gen(genMaxSong), "song") THEN
    songname = ""
    importsong_save_song_data songname, songnum
   END IF
   state.need_update = YES
  END IF
 END IF
 IF enter_space_click(state) THEN
  IF state.pt = 0 THEN EXIT DO
  IF state.pt = 3 THEN
   importsong_import_song_file songname, songfile, bamfile, songnum
   state.need_update = YES
  END IF
  IF state.pt = 4 AND songfile <> "" THEN importsong_exportsong songfile, bamfile, file_ext, songname
  IF state.pt = 5 AND songfile <> "" THEN  'delete song
   IF yesno("Really delete this song?", NO, NO) THEN
    music_stop
    'closemusic  'music_stop not always enough to cause the music backend to let go of the damn file!
    'setupmusic
    delete_song songnum, songfile, bamfile
    state.need_update = YES
   END IF
  END IF
  IF state.pt = 6 THEN  'delete BAM fallback
   IF yesno("Really delete this BAM song?", NO, NO) THEN
    safekill bamfile
    state.need_update = YES
    state.pt = 0
   END IF
  END IF
 END IF

 ' Update seek bar position...
 seekbar.y = vpages(vpage)->h - 15 - seekbar.h
 seekbar.w = vpages(vpage)->w - 40
 ' ...and hover
 DIM seekpos as double = -1.0  'Also used for mouse hovering
 'Allow clicking off the left side of the bar, which seeks to 0.00
 'IF rect_collide_point(seekbar, readmouse.pos) THEN
 IF rect_collide_point(XYWH(0, seekbar.y, vpages(vpage)->w, seekbar.h), readmouse.pos) THEN
  'Older SDL_mixer allow seeking but don't have Mix_MusicDuration or Mix_GetMusicPosition,
  'so just use a 10min dummy length so that the seek bar still allows seeking.
  DIM songend as double = IIF(songlength > 0, songlength, 600)
  seekpos = songend * (readmouse.x - seekbar.x) / seekbar.w
  seekpos = bound(seekpos, 0.0, songend)
 END IF
 ' Seek bar clicking
 IF seekpos >= 0.0 THEN
  IF (readmouse.clicks AND mouseLeft) ORELSE (readmouse.buttons AND mouseLeft ANDALSO readmouse.moved) THEN
   music_settime(seekpos)
  END IF
 END IF

 IF state.need_update THEN
  state.need_update = NO
  'Plays the song
  importsong_get_song_info songname, songfile, bamfile, songnum, file_ext, menu(), metadata, songlength, selectable(), state
 END IF

 clearpage dpage
 standardmenu menu(), state, , , dpage
 'Allow newlines in metadata but don't wrap, to cut off long comments
 edgeprint metadata, pMenuX, pMenuY + 9 * (UBOUND(menu) + 2), uilook(uiMenuItem), dpage, YES, YES
 edgeprint "F2: Preview volume " & CINT(100 * get_music_volume()) & "%", pInfoRight, pInfoY, uilook(uiMenuItem), dpage
 importsong_draw_seek_bar seekbar, seekpos, songlength, dpage

 SWAP vpage, dpage
 setvispage vpage
 dowait
LOOP
importsong_save_song_data songname, songnum
music_stop
RETURN songnum
END FUNCTION

SUB importsong_import_song_file (songname as string, songfile as string, bamfile as string, byval songnum as integer)
 STATIC default as string
 music_stop
 'closemusic  'music_stop not always enough to cause the music backend to let go of the damn file!
 'setupmusic

 'browse for new song
 DIM sourcesong as string = browse(browseMusic, default, "", "browse_import_song")
 IF sourcesong = "" THEN EXIT SUB

 'Get song name
 DIM newname as string = decode_filename(trimextension(trimpath(sourcesong)))

 'Convert WAV/MP3
 DIM oggtemp as string
 IF getmusictype(sourcesong) = FORMAT_MP3 THEN
  import_convert_mp3 sourcesong, oggtemp
 ELSEIF getmusictype(sourcesong) = FORMAT_WAV THEN
  import_convert_wav sourcesong, oggtemp
 ELSE
  oggtemp = ""
 END IF

 'If conversion to ogg failed, exit
 IF sourcesong = "" THEN
  EXIT SUB
 END IF

 delete_song songnum, songfile, bamfile

 songname = newname

 'generate lump name
 DIM extension as string = LCASE(justextension(sourcesong))
 IF extension = "bam" AND songnum <= 99 THEN
  songfile = game + "." & songnum
 ELSE
  songfile = workingdir & SLASH & "song" & songnum & "." & extension
 END IF

 'Copy in new lump (this implicitly sends a notification to Game if it's been spawned)
 writeablecopyfile sourcesong, songfile

 IF oggtemp <> "" THEN killfile oggtemp

 importsong_save_song_data songname, songnum
END SUB

SUB importsong_get_song_info (songname as string, songfile as string, bamfile as string, byval songnum as integer, file_ext as string, menu() as string, metadata as string, byref songlength as double, selectable() as bool, state as MenuState)
 music_stop

 'TODO: this is redundant to find_music_lump and getmusictype

 DIM temp as string
 '-- first job: find the song's name
 temp = workingdir & SLASH & "song" & songnum
 songfile = ""
 file_ext = ""
 DIM songtype as string = "NO FILE"
 '-- BAM special case and least desirable, so check first and override
 IF songnum > 99 THEN
  IF isfile(temp & ".bam") THEN
   file_ext = ".bam"
   songfile = temp & file_ext
   songtype = "Bob's Adlib Music (BAM)"
  END IF
 ELSE
  IF isfile(game & "." & songnum) THEN
   file_ext = ".bam"
   songfile = game & "." & songnum
   songtype = "Bob's Adlib Music (BAM)"
  END IF
 END IF
 bamfile = songfile

 IF isfile(temp & ".ogg") THEN
  file_ext = ".ogg"
  songfile = temp & file_ext
  songtype = "OGG Vorbis (OGG)"
 ELSEIF isfile(temp & ".s3m") THEN
  file_ext = ".s3m"
  songfile = temp & file_ext
  songtype = "Screamtracker (S3M)"
 ELSEIF isfile(temp & ".it") THEN
  file_ext = ".it"
  songfile = temp & file_ext
  songtype = "Impulse Tracker (IT)"
 ELSEIF isfile(temp & ".xm") THEN
  file_ext = ".xm"
  songfile = temp & file_ext
  songtype = "Extended Module (XM)"
 ELSEIF isfile(temp & ".mod") THEN
  file_ext = ".mod"
  songfile = temp & file_ext
  songtype = "Module (MOD)"
 ELSEIF isfile(temp & ".mp3") THEN ' Obsolete. only present in some Ubersetzung WIP games
  file_ext = ".mp3"
  songfile = temp & file_ext
  songtype = "MPEG Layer III (MP3)"
 ELSEIF isfile(temp & ".flac") THEN ' Not supported by most builds, and importing isn't permitted
  file_ext = ".flac"
  songfile = temp & file_ext
  songtype = "FLAC"
 ELSEIF isfile(temp & ".opus") THEN ' Not supported by most builds, and importing isn't permitted
  file_ext = ".opus"
  songfile = temp & file_ext
  songtype = "OPUS"
 ELSEIF isfile(temp & ".mid") THEN
  file_ext = ".mid"
  songfile = temp & file_ext
  songtype = "MIDI Music (MID)"
 END IF
 '--add more formats here

 songname = getsongname(songnum)

 IF songfile <> "" THEN '--song exists
  loadsong songfile
  songlength = music_getlength
 ELSE
  songname = ""
  songlength = -1.0
 END IF

 menu(1) = "<- Song " & songnum & " of " & gen(genMaxSong) & " ->"

 IF songfile = "" THEN
  menu(2) = "-Unused-"
  state.last = 3
  metadata = ""

 ELSE
  menu(2) = "Name: " & songname
  IF bamfile <> songfile AND bamfile <> "" THEN
   menu(6) = "Delete BAM fallback"
   state.last = 6
  ELSE
   state.last = 5
  END IF

  '-- add author, length, etc, info here
  DIM extended_metadata as string
  DIM metadata_duration as double
  IF file_ext = ".mp3" THEN
   extended_metadata = read_mp3_metadata(songfile, songtype, metadata_duration)
  END IF
  IF file_ext = ".ogg" THEN
   extended_metadata = read_ogg_metadata(songfile, metadata_duration)
  END IF
  IF songlength < 0.0 ANDALSO metadata_duration > 0.0 THEN songlength = metadata_duration

  metadata  = "Type:     " & songtype & !"\n"
  'read_ogg/mp3_metadata also add Length
  IF INSTR(metadata, "Length: ") = 0 ANDALSO songlength > 0 THEN
   metadata &= "Length:   " & format_duration(songlength) & !"\n" '& " (" & IIF(music_seekable, "", "Not ") & !"Seekable)\n"
  END IF
  metadata &= "Filesize: " & filesize(songfile) & !"\n"

  IF bamfile <> songfile AND bamfile <> "" THEN
   metadata &= "BAM fallback exists. Filesize: " & filesize(bamfile) & !"\n"
  END IF
  metadata &= extended_metadata

  IF (getmusictype(songfile) AND music_supported_formats()) = 0 THEN
   'Note: music_sdl might report it doesn't support MP3 although it will play it
   metadata &= !"The music backend can't (reliably) play this file type.\n"
  END IF
 END IF

 REDIM PRESERVE selectable(UBOUND(menu))
 FOR i as integer = 0 TO UBOUND(selectable)
  selectable(i) = YES
 NEXT
END SUB

'songfile: Complete path to the song file to be exported
'bamfile: The .bam fallback file, if any
SUB importsong_exportsong(songfile as string, bamfile as string, file_ext as string, songname as string)
 IF bamfile <> songfile AND LEN(bamfile) THEN
  DIM choice as integer = twochoice("Export which version of this song?", _
                                    file_ext + " file", _
                                    ".bam fallback file")
  IF choice = 1 THEN file_ext = ".bam" : songfile = bamfile
  IF choice = -1 THEN EXIT SUB
 END IF
 DIM query as string = "Name of file to export to?"
 DIM outfile as string = inputfilename(query, file_ext, "", "input_file_export_song", songname)
 IF outfile = "" THEN EXIT SUB
 writeablecopyfile songfile, outfile & file_ext
END SUB

SUB delete_song (byval songnum as integer, songfile as string, bamfile as string)
 IF LEN(songfile) = 0 THEN EXIT SUB
#IFNDEF NO_TEST_GAME
 #IFDEF __FB_WIN32__
  'Only needed on windows, and not currently implemented on unix anyway
  IF channel_to_Game THEN
   'Close Music message
   DIM msg as string = "CM " & songnum
   IF channel_write_line(channel_to_Game, msg) THEN
    channel_wait_for_msg(channel_to_Game, "CM ", "", 1500)
   END IF
  END IF
 #ENDIF
#ENDIF
 safekill songfile
 IF LEN(bamfile) THEN safekill bamfile
 'FIXME: handle deleting from rpgdirs (bug 247)... and the same for soundeffects

 IF channel_to_Game THEN send_lump_modified_msg(songfile)  'only need to send any valid filename for this song
END SUB

SUB importsong_save_song_data(songname as string, byval songnum as integer)
 DIM songbuf(dimbinsize(binSONGDATA)) as integer
 writebinstring songname, songbuf(), 0, 30
 storerecord songbuf(), workingdir & SLASH & "songdata.bin", curbinsize(binSONGDATA) \ 2, songnum
END SUB

SUB export_songlist()
 DIM filename as string
 filename = inputfilename("File to export song list to?", ".txt", "", "")
 IF LEN(filename) = 0 THEN EXIT SUB
 DIM fh as integer
 IF OPENFILE(filename + ".txt", FOR_OUTPUT, fh) THEN
  showerror "Couldn't open file"
  EXIT SUB
 END IF
 FOR num as integer = 0 TO gen(genMaxSong)
  PRINT #fh, getsongname(num, YES)
 NEXT
 CLOSE fh
END SUB


'==========================================================================================
'                                Sound Effects Import/Editor
'==========================================================================================

SUB sfx_editor_main ()
 IF read_config_bool("thingbrowser.enable_top_level", YES) THEN
  DIM b as SfxBrowser
  b.browse(-1, , @importsfx)
 ELSE
  importsfx 0
 END IF
END SUB

FUNCTION sfx_picker (recindex as integer = -1) as integer
 DIM b as SfxBrowser
 RETURN b.browse(recindex, , @importsfx, NO)
END FUNCTION

FUNCTION sfx_picker_or_none (recindex as integer = -1) as integer
 DIM b as SfxBrowser
 RETURN b.browse(recindex - 1, YES , @importsfx, NO) + 1
END FUNCTION

FUNCTION importsfx (byval sfxnum as integer) as integer
'sfxnum is the sfx to start with, or > max to add a new one.
'Return value is the last selected sfx

IF sfxnum > gen(genMaxSFX) THEN
 'Requested a new one
 gen(genMaxSFX) += 1
 sfxnum = gen(genMaxSFX)
END IF

REDIM menu(6) as string
REDIM selectable(6) as bool  'No actual purpose
menu(0) = "Previous Menu"
menu(3) = "Import Sound..."
menu(4) = "Export Sound..."
menu(5) = "Delete Sound"
menu(6) = "Play Sound"

DIM state as MenuState
state.pt = 1
state.size = 24
state.autosize = YES
state.autosize_ignore_lines = 1
state.last = UBOUND(menu)

DIM sfxname as string = ""
DIM sfxfile as string = ""
DIM newsfx as integer
DIM file_ext as string
DIM metadata as string
importsfx_get_sfx_info sfxname, sfxfile, sfxnum, file_ext, menu(), metadata, selectable(), state

setkeys YES
DO
 setwait 55
 setkeys YES
 IF keyval(ccCancel) > 1 THEN EXIT DO
 IF keyval(scF1) > 1 THEN show_help "import_sfx"
 IF keyval(scF2) > 1 THEN Custom_volume_menu
 IF keyval(scF3) > 1 THEN music_backend_menu

 usemenu state, selectable()

 IF state.pt = 2 AND sfxfile <> "" THEN
  strgrabber sfxname, 30
  menu(2) = "Name: " + sfxname
 ELSE
  '-- check for switching sfx
  newsfx = sfxnum
  IF intgrabber(newsfx, 0, gen(genMaxSFX), scLeftCaret, scRightCaret) THEN
   importsfx_save_sfx_data sfxname, sfxnum
   freesfx sfxnum
   sfxnum = newsfx
   importsfx_get_sfx_info sfxname, sfxfile, sfxnum, file_ext, menu(), metadata, selectable(), state
  END IF
  IF keyval(ccLeft) > 1 AND sfxnum > 0 THEN
   importsfx_save_sfx_data sfxname, sfxnum
   freesfx sfxnum
   sfxnum -= 1
   importsfx_get_sfx_info sfxname, sfxfile, sfxnum, file_ext, menu(), metadata, selectable(), state
  END IF
  IF keyval(ccRight) > 1 AND sfxnum < maxMaxSFX THEN
   importsfx_save_sfx_data sfxname, sfxnum
   freesfx sfxnum
   sfxnum += 1
   IF needaddset(sfxnum, gen(genMaxSFX), "sfx") THEN sfxname = ""
   importsfx_get_sfx_info sfxname, sfxfile, sfxnum, file_ext, menu(), metadata, selectable(), state
  END IF
 END IF
 IF enter_space_click(state) THEN
  SELECT CASE state.pt
  CASE 0   'quit
    EXIT DO
  CASE 3   'import
    freesfx sfxnum
    importsfx_importsfxfile sfxname, sfxfile, sfxnum, file_ext
    importsfx_get_sfx_info sfxname, sfxfile, sfxnum, file_ext, menu(), metadata, selectable(), state
  CASE 4   'export
    IF sfxfile <> "" THEN importsfx_exportsfx sfxfile, file_ext, sfxname
  CASE 5   'delete sfx
    IF sfxfile <> "" THEN
      IF yesno("Really delete this sound?", NO, NO) THEN
        freesfx sfxnum
        safekill sfxfile
        importsfx_get_sfx_info sfxname, sfxfile, sfxnum, file_ext, menu(), metadata, selectable(), state
      END IF
    END IF
  CASE 1, 6
    IF sfxfile <> "" THEN 'play sfx
      playsfx sfxnum, 0
    END IF

  END SELECT
 END IF

 clearpage dpage
 standardmenu menu(), state, , , dpage
 'Allow newlines in metadata but don't wrap, to cut off long comments
 edgeprint metadata, pMenuX, pMenuY + 9 * (UBOUND(menu) + 2), uilook(uiMenuItem), dpage, YES, YES
 edgeprint "F2: Preview volume " & CINT(100 * get_global_sfx_volume()) & "%", pInfoRight, pInfoY, uilook(uiMenuItem), dpage

 SWAP vpage, dpage
 setvispage vpage
 dowait
LOOP
importsfx_save_sfx_data sfxname, sfxnum
freesfx sfxnum
RETURN sfxnum
END FUNCTION

SUB importsfx_importsfxfile(sfxname as string, sfxfile as string, byval sfxnum as integer, file_ext as string)
 STATIC default as string

 DIM sourcesfx as string = browse(browseSfx, default, "", "browse_import_sfx")
 IF sourcesfx = "" THEN EXIT SUB

 '-- get name (before sourcesfx is modified)
 DIM newname as string = decode_filename(trimextension(trimpath(sourcesfx)))

 'Convert MP3
 DIM oggtemp as string
 IF getmusictype(sourcesfx) = FORMAT_MP3 THEN
  import_convert_mp3 sourcesfx, oggtemp
 ELSEIF getmusictype(sourcesfx) = FORMAT_WAV THEN
  import_convert_wav sourcesfx, oggtemp
 ELSE
  oggtemp = ""
 END IF

 'If conversion to ogg failed, exit
 IF sourcesfx = "" THEN EXIT SUB

 'Delete the old file. We cannot assume it will be overwritten because the extension might change
 IF sfxfile <> "" THEN safekill sfxfile

 sfxname = newname

 '-- calculate lump name
 sfxfile = workingdir & SLASH & "sfx" & sfxnum & "." & LCASE(justextension(sourcesfx))

 '--copy in the new lump
 writeablecopyfile sourcesfx, sfxfile

 IF oggtemp <> "" THEN killfile oggtemp

 '--save and update
 importsfx_save_sfx_data sfxname, sfxnum
END SUB

SUB importsfx_exportsfx(sfxfile as string, file_ext as string, sfxname as string)
 DIM query as string = "Name of file to export to?"
 DIM outfile as string = inputfilename(query, file_ext, "", "input_file_export_sfx", sfxname)
 IF outfile = "" THEN EXIT SUB
 writeablecopyfile sfxfile, outfile & file_ext
END SUB

SUB importsfx_save_sfx_data(sfxname as string, byval sfxnum as integer)
 DIM sfxbuf(dimbinsize(binSFXDATA)) as integer
 writebinstring sfxname, sfxbuf(), 0, 30
 storerecord sfxbuf(), workingdir & SLASH & "sfxdata.bin", curbinsize(binSFXDATA) \ 2, sfxnum
END SUB

SUB importsfx_get_sfx_info(sfxname as string, sfxfile as string, byval sfxnum as integer, file_ext as string, menu() as string, metadata as string, selectable() as bool, state as MenuState)
 '-- first job: find the sfx's name
 DIM temp as string = workingdir & SLASH & "sfx" & sfxnum
 DIM sfxtype as string = "NO FILE"
 
 sfxfile = "" ' this will be rebuilt below

 IF isfile(temp & ".ogg") THEN
  file_ext = ".ogg"
  sfxfile = temp & file_ext
  sfxtype = "OGG Vorbis (OGG)"
 ELSEIF isfile(temp & ".wav") THEN ' Obsolete, only present in Pre-Ubersetzung games
  file_ext = ".wav"
  sfxfile = temp & file_ext
  sfxtype = "Waveform (WAV)"
 ELSEIF isfile(temp & ".mp3") THEN ' Obsolete, only present in some Ubersetzung WIP games
  file_ext = ".mp3"
  sfxfile = temp & file_ext
  sfxtype = "MPEG Layer III (MP3)"
 ELSEIF isfile(temp & ".flac") THEN ' Not supported in most builds
  file_ext = ".flac"
  sfxfile = temp & file_ext
  sfxtype = "FLAC"
 END IF

 '--add more formats here

 if sfxfile <> "" then
  'playsfx sfxnum, 0
  sfxname = getsfxname(sfxnum)
 ELSE '--sfx doesn't exist
  sfxname = ""
 END IF

 menu(1) = "<- SFX " & sfxnum & " of " & gen(genMaxSFX) & " ->"
 IF sfxfile = "" THEN
  menu(2) = "-Unused-"
  state.last = 3  ' Hide Export/Delete/Play Sound

  metadata = ""
 ELSE
  menu(2) = "Name: " & sfxname
  state.last = 6  ' Show Export/Delete/Play Sound

  '-- add author, length, etc, info here
  DIM extended_metadata as string
  IF file_ext = ".mp3" THEN
   extended_metadata = read_mp3_metadata(sfxfile, sfxtype)
  END IF
  IF file_ext = ".ogg" THEN
   extended_metadata = read_ogg_metadata(sfxfile)
  END IF

  metadata  = "Type:     " & sfxtype & !"\n"
  metadata &= "Filesize: " & filesize(sfxfile) & !"\n"
  IF filelen(sfxfile) > 1 * 1024 * 1024 THEN
   metadata &= !"Large sound effects can cause pauses when played!\n"
  END IF

  metadata &= extended_metadata

  IF (getmusictype(sfxfile) AND sound_supported_formats()) = 0 THEN
   metadata &= !"This engine build can't play this file\n"
  END IF
 END IF

 REDIM selectable(UBOUND(menu))
 FOR i as integer = 0 TO 6
  selectable(i) = YES
 NEXT
END SUB


'==========================================================================================
'                                  Global Music/Sounds
'==========================================================================================

SUB generalmusicsfxmenu ()
  CONST menusize as integer = 18
  CONST lastmusicitem as integer = 3
  DIM as string menu(menusize), disp(menusize), menu_display(menusize)
  DIM as integer index(1 to menusize) = { _
          genTitleMus, genBatMus, genVictMus, genAcceptSFX, genCancelSFX, _
          genCursorSFX, genTextboxLine, genDefaultDeathSFX, genItemLearnSFX, genCantLearnSFX, _
          genBuySFX, genHireSFX, genSellSFX, genCantBuySFX, genCantSellSFX, _
          genStealSuccessSFX, genStealFailSFX, genStealNoItemSFX _
  }
  ' mins() stores the minimum allowed values of the gen() elements,
  ' which is offset by 1 from the song/sfx number
  DIM mins(1 TO menusize) as integer
  mins(2) = -1  'Default battle music can be -1, "same as map"

  disp(0) = "Previous Menu" 'don't need menu(0)
  menu(1) = "Title Music: "
  menu(2) = "Default Battle Music: "
  menu(3) = "Battle Victory Music: "
  menu(4) = "Accept Sound: "
  menu(5) = "Cancel Sound: "
  menu(6) = "Cursor Sound: "
  menu(7) = "Textbox Line Sound: "
  menu(8) = "Default Enemy Death: "
  menu(9) = "Learnt From Item Sound: "
  menu(10) = "Can't Learn From Item Sound: "
  menu(11) = "Buy Item Sound: "
  menu(12) = "Hire Hero Sound: "
  menu(13) = "Sell Item Sound: "
  menu(14) = "Can't Buy Sound: "
  menu(15) = "Can't Sell Sound: "
  menu(16) = "Steal Success Sound: "
  menu(17) = "Steal Failure Sound: "
  menu(18) = "Steal No-Item Sound: "

  DIM selectst as SelectTypeState
  DIM state as MenuState
  state.size = 24
  state.last = menusize
  state.need_update = YES

  setkeys YES
  DO
    setwait 55
    setkeys YES

    IF keyval(ccCancel) > 1 THEN EXIT DO
    IF keyval(scF1) > 1 THEN show_help "general_music_sfx"
    IF keyval(scF2) > 1 THEN Custom_volume_menu
    usemenu state

    IF enter_space_click(state) THEN
      SELECT CASE state.pt
      CASE 0
        EXIT DO
      CASE 1 TO lastmusicitem
        gen(index(state.pt)) = song_picker_or_none(gen(index(state.pt)))
        state.need_update = YES
        ' TODO: it's a bit silly to play the song/sfx only after leaving the browser, because
        ' it plays in the browser anyway
        IF gen(index(state.pt)) > 0 THEN
         playsongnum gen(index(state.pt)) - 1
        END IF
      CASE lastmusicitem + 1 TO state.last
        gen(index(state.pt)) = sfx_picker_or_none(gen(index(state.pt)))
        state.need_update = YES
        IF gen(index(state.pt)) > 0 THEN
         playsfx gen(index(state.pt)) - 1
        END IF
      END SELECT
    END IF

    SELECT CASE state.pt
    CASE 1 TO lastmusicitem
      'Music
      IF zintgrabber(gen(index(state.pt)), mins(state.pt) - 1, gen(genMaxSong)) THEN
        music_stop
        state.need_update = YES
      END IF
    CASE lastmusicitem + 1 TO state.last
      'Sound effect
      IF zintgrabber(gen(index(state.pt)), mins(state.pt) - 1, gen(genMaxSFX)) THEN
        resetsfx
        state.need_update = YES
      END IF
    END SELECT

    IF state.need_update THEN
      state.need_update = NO
      FOR idx as integer = 1 to state.last
        DIM value as integer = gen(index(idx)) - 1
        IF value >= 0 THEN
          IF idx <= lastmusicitem THEN
            disp(idx) = menu(idx) & getsongname(value, -1)  'prefixes number
          ELSE
            disp(idx) = menu(idx) & value & " " & getsfxname(value)
          END IF
        ELSEIF value = -1 THEN
          IF index(idx) = genBatMus THEN
            disp(idx) = menu(idx) & "Silence"
          ELSE
            disp(idx) = menu(idx) & "None"
          END IF
        ELSEIF value = -2 THEN
          disp(idx) = menu(idx) & "Same as map"
        END IF
      NEXT
    END IF

    IF select_by_typing(selectst, NO) THEN
      select_on_word_boundary disp(), selectst, state
    END IF

    clearpage dpage
    highlight_menu_typing_selection disp(), menu_display(), selectst, state
    standardmenu menu_display(), state, , , dpage

    SELECT CASE state.pt
     CASE 1 TO lastmusicitem
       edgeprint "F2: Preview volume " & CINT(100 * get_music_volume()) & "%", pInfoRight, pInfoY, uilook(uiMenuItem), dpage
     CASE IS > lastmusicitem
       edgeprint "F2: Preview volume " & CINT(100 * get_global_sfx_volume()) & "%", pInfoRight, pInfoY, uilook(uiMenuItem), dpage
     END SELECT

    SWAP vpage, dpage
    setvispage vpage
    dowait
  LOOP
  music_stop
  resetsfx
END SUB
