From 4f49e51b9796e603b3cebf7c102c5712d083b292 Mon Sep 17 00:00:00 2001 From: tobigun Date: Thu, 23 Jul 2009 19:34:14 +0000 Subject: Midi to USDX converter fixed git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/branches/experimental@1896 b956fd51-792f-4845-bead-9b4dfca2ff2c --- unicode/src/screens/UScreenEditConvert.pas | 454 ++++++++++++++++++----------- 1 file changed, 289 insertions(+), 165 deletions(-) (limited to 'unicode/src/screens/UScreenEditConvert.pas') diff --git a/unicode/src/screens/UScreenEditConvert.pas b/unicode/src/screens/UScreenEditConvert.pas index 2705a5b7..b4417cc7 100644 --- a/unicode/src/screens/UScreenEditConvert.pas +++ b/unicode/src/screens/UScreenEditConvert.pas @@ -33,6 +33,19 @@ unit UScreenEditConvert; * http://www.midi.org/techspecs/rp26.php * MIDI File Format * http://www.sonicspot.com/guide/midifiles.html + * KMIDI File Format + * http://gnese.free.fr/Projects/KaraokeTime/Fichiers/karfaq.html + * http://journals.rpungin.fotki.com/karaoke/category/midi + * + * There are two widely spread karaoke formats: + * - KMIDI (.kar), an inofficial midi extension by Tune 1000 + * - Standard Midi files with lyric meta-tags (SMF with lyrics, .mid). + * + * KMIDI uses two tracks, the first just contains a header (mostly track 2) and + * the second the lyrics (track 3). It uses text meta tags for the lyrics. + * SMF uses just one track (normally track 1) and uses lyric meta tags for storage. + * + * Most files are in the KMIDI format. Some Midi files contain both lyric types. *} interface @@ -67,14 +80,16 @@ type Len: real; Data1: integer; Data2: integer; - Str: AnsiString; + Str: UTF8String; // normally ASCII end; + TLyricType = (ltKMIDI, ltSMFLyric); + TTrack = record Note: array of TMidiNote; - Name: AnsiString; - Status: set of (tsNotes, tsLyrics); - LyricType: set of (ltKMIDI, ltSMFLyric); + Name: UTF8String; // normally ASCII + Status: set of (tsNotes, tsLyrics); //< track contains notes, lyrics or both + LyricType: set of TLyricType; NoteType: (ntNone, ntAvail); end; @@ -82,7 +97,7 @@ type Start: integer; Len: integer; Tone: integer; - Lyric: AnsiString; + Lyric: UTF8String; NewSentence: boolean; end; @@ -96,23 +111,19 @@ type ColB: array[0..100] of real; Len: real; SelTrack: integer; // index of selected track - //FileName: IPath; + fFileName: IPath; {$IFDEF UseMIDIPort} MidiFile: TMidiFile; - MidiTrack: TMidiTrack; - MidiEvent: PMidiEvent; MidiOut: TMidiOutput; {$ENDIF} - Song: TSong; - Lines: TLines; BPM: real; Ticks: real; Note: array of TNote; - procedure AddLyric(Start: integer; Text: AnsiString); - procedure Extract; + procedure AddLyric(Start: integer; LyricType: TLyricType; Text: UTF8String); + procedure Extract(out Song: TSong; out Lines: TLines); {$IFDEF UseMIDIPort} procedure MidiFile1MidiEvent(event: PMidiEvent); @@ -128,9 +139,6 @@ type procedure OnHide; override; end; -var - ConversionFileName: IPath; - implementation uses @@ -144,6 +152,7 @@ uses UMain, UPathUtils, USkins, + ULanguage, UTextEncoding, UUnicodeUtils; @@ -162,9 +171,14 @@ const MIDI_META_LYRICS = $5; function TScreenEditConvert.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; PressedDown: boolean): boolean; +{$IFDEF UseMIDIPort} var SResult: TSaveSongResult; Playing: boolean; + MidiTrack: TMidiTrack; + Song: TSong; + Lines: TLines; +{$ENDIF} begin Result := true; if (PressedDown) then @@ -184,7 +198,8 @@ begin SDLK_BACKSPACE : begin {$IFDEF UseMIDIPort} - MidiFile.StopPlaying; + if (MidiFile <> nil) then + MidiFile.StopPlaying; {$ENDIF} AudioPlayback.PlaySound(SoundLib.Back); FadeTo(@ScreenEdit); @@ -195,82 +210,96 @@ begin if Interaction = 0 then begin AudioPlayback.PlaySound(SoundLib.Start); + ScreenOpen.Filename := GamePath.Append('file.mid'); ScreenOpen.BackScreen := @ScreenEditConvert; FadeTo(@ScreenOpen); - end; - - if Interaction = 1 then + end + else if Interaction = 1 then begin {$IFDEF UseMIDIPort} - MidiFile.OnMidiEvent := MidiFile1MidiEvent; - //MidiFile.GoToTime(MidiFile.GetTrackLength div 2); - MidiFile.StartPlaying; + if (MidiFile <> nil) then + begin + MidiFile.OnMidiEvent := MidiFile1MidiEvent; + //MidiFile.GoToTime(MidiFile.GetTrackLength div 2); + MidiFile.StartPlaying; + end; {$ENDIF} - end; - - if Interaction = 2 then + end + else if Interaction = 2 then begin {$IFDEF UseMIDIPort} - MidiFile.OnMidiEvent := nil; - MidiFile.StartPlaying; + if (MidiFile <> nil) then + begin + MidiFile.OnMidiEvent := nil; + MidiFile.StartPlaying; + end; {$ENDIF} - end; - - if Interaction = 3 then + end + else if Interaction = 3 then begin + {$IFDEF UseMIDIPort} if CountSelectedTracks > 0 then begin - Extract; - SResult := SaveSong(Song, Lines, ConversionFileName.SetExtension('.txt'), + Extract(Song, Lines); + SResult := SaveSong(Song, Lines, fFileName.SetExtension('.txt'), false); - if (SResult <> ssrOK) then - begin - ScreenPopupError.ShowPopup('Could not save file'); - end; + FreeAndNil(Song); + if (SResult = ssrOK) then + ScreenPopupInfo.ShowPopup(Language.Translate('INFO_FILE_SAVED')) + else + ScreenPopupError.ShowPopup(Language.Translate('ERROR_SAVE_FILE_FAILED')); + end + else + begin + ScreenPopupError.ShowPopup(Language.Translate('ERROR_EDITOR_NO_TRACK_SELECTED')); end; + {$ENDIF} end; end; SDLK_SPACE: begin - if (Tracks[SelTrack].NoteType = ntAvail) and - (Tracks[SelTrack].LyricType <> []) then - begin - if (Tracks[SelTrack].Status = []) then - Tracks[SelTrack].Status := [tsNotes] - else if (Tracks[SelTrack].Status = [tsNotes]) then - Tracks[SelTrack].Status := [tsLyrics] - else if (Tracks[SelTrack].Status = [tsLyrics]) then - Tracks[SelTrack].Status := [tsNotes, tsLyrics] - else if (Tracks[SelTrack].Status = [tsNotes, tsLyrics]) then - Tracks[SelTrack].Status := []; - end - else if (Tracks[SelTrack].NoteType = ntAvail) then - begin - if (Tracks[SelTrack].Status = []) then - Tracks[SelTrack].Status := [tsNotes] - else - Tracks[SelTrack].Status := []; - end - else if (Tracks[SelTrack].LyricType <> []) then + {$IFDEF UseMIDIPort} + if (MidiFile <> nil) then begin - if (Tracks[SelTrack].Status = []) then - Tracks[SelTrack].Status := [tsLyrics] + if (Tracks[SelTrack].NoteType = ntAvail) and + (Tracks[SelTrack].LyricType <> []) then + begin + if (Tracks[SelTrack].Status = []) then + Tracks[SelTrack].Status := [tsNotes] + else if (Tracks[SelTrack].Status = [tsNotes]) then + Tracks[SelTrack].Status := [tsLyrics] + else if (Tracks[SelTrack].Status = [tsLyrics]) then + Tracks[SelTrack].Status := [tsNotes, tsLyrics] + else if (Tracks[SelTrack].Status = [tsNotes, tsLyrics]) then + Tracks[SelTrack].Status := []; + end + else if (Tracks[SelTrack].NoteType = ntAvail) then + begin + if (Tracks[SelTrack].Status = []) then + Tracks[SelTrack].Status := [tsNotes] + else + Tracks[SelTrack].Status := []; + end + else if (Tracks[SelTrack].LyricType <> []) then + begin + if (Tracks[SelTrack].Status = []) then + Tracks[SelTrack].Status := [tsLyrics] + else + Tracks[SelTrack].Status := []; + end; + + Playing := (MidiFile.GetCurrentTime > 0); + MidiFile.StopPlaying(); + MidiTrack := MidiFile.GetTrack(SelTrack); + if tsNotes in Tracks[SelTrack].Status then + MidiTrack.OnMidiEvent := MidiFile1MidiEvent else - Tracks[SelTrack].Status := []; + MidiTrack.OnMidiEvent := nil; + if (Playing) then + MidiFile.ContinuePlaying(); end; - - {$IFDEF UseMIDIPort} - Playing := (MidiFile.GetCurrentTime > 0); - MidiFile.StopPlaying(); - MidiTrack := MidiFile.GetTrack(SelTrack); - if tsNotes in Tracks[SelTrack].Status then - MidiTrack.OnMidiEvent := MidiFile1MidiEvent - else - MidiTrack.OnMidiEvent := nil; - if (Playing) then - MidiFile.ContinuePlaying(); {$ENDIF} end; @@ -300,38 +329,62 @@ begin end; end; -procedure TScreenEditConvert.AddLyric(Start: integer; Text: AnsiString); +procedure TScreenEditConvert.AddLyric(Start: integer; LyricType: TLyricType; Text: UTF8String); var N: integer; begin - for N := 0 to High(Note) do + // find corresponding note + N := 0; + while (N <= High(Note)) do begin if Note[N].Start = Start then - begin - // Line Feed -> end of paragraph - if Copy(Text, 1, 1) = #$0A then - Delete(Text, 1, 1); - // Carriage Return -> end of line - if Copy(Text, 1, 1) = #$0D then - begin - Delete(Text, 1, 1); - Note[N].NewSentence := true; - end; - - // overwrite lyric or append - if Note[N].Lyric = '-' then - Note[N].Lyric := Text - else - Note[N].Lyric := Note[N].Lyric + Text; + Break; + Inc(N); + end; - Exit; + // check if note was found + if (N > High(Note)) then + Exit; + + // set text + if (LyricType = ltKMIDI) then + begin + // end of paragraph + if Copy(Text, 1, 1) = '\' then + begin + Delete(Text, 1, 1); + end + // end of line + else if Copy(Text, 1, 1) = '/' then + begin + Delete(Text, 1, 1); + Note[N].NewSentence := true; + end; + end + else // SMFLyric + begin + // Line Feed -> end of paragraph + if Copy(Text, 1, 1) = #$0A then + begin + Delete(Text, 1, 1); + end + // Carriage Return -> end of line + else if Copy(Text, 1, 1) = #$0D then + begin + Delete(Text, 1, 1); + Note[N].NewSentence := true; end; end; - DebugWriteln('Missing: ' + Text); + // overwrite lyric or append + if Note[N].Lyric = '-' then + Note[N].Lyric := Text + else + Note[N].Lyric := Note[N].Lyric + Text; end; -procedure TScreenEditConvert.Extract; +procedure TScreenEditConvert.Extract(out Song: TSong; out Lines: TLines); + var T: integer; C: integer; @@ -340,6 +393,8 @@ var NoteTemp: TNote; Move: integer; Max, Min: integer; + LyricType: TLyricType; + Text: UTF8String; begin // song info Song := TSong.Create(); @@ -347,7 +402,6 @@ begin Song.Resolution := 4; SetLength(Song.BPM, 1); Song.BPM[0].BPM := BPM*4; - SetLength(Note, 0); // extract notes @@ -371,18 +425,51 @@ begin end; end; - // extract lyrics + // extract lyrics (and artist + title info) for T := 0 to High(Tracks) do begin - if tsLyrics in Tracks[T].Status then + if not (tsLyrics in Tracks[T].Status) then + Continue; + + for N := 0 to High(Tracks[T].Note) do begin - for N := 0 to High(Tracks[T].Note) do + if (Tracks[T].Note[N].Event = MIDI_EVENT_META) then begin - if (Tracks[T].Note[N].Event = MIDI_EVENT_META) and - (Tracks[T].Note[N].Data1 = MIDI_META_LYRICS) then + // determine and validate lyric meta tag + if (ltKMIDI in Tracks[T].LyricType) and + (Tracks[T].Note[N].Data1 = MIDI_META_TEXT) then begin - AddLyric(Round(Tracks[T].Note[N].Start / Ticks), Tracks[T].Note[N].Str); + Text := Tracks[T].Note[N].Str; + + // check for meta info + if (Length(Text) > 2) and (Text[1] = '@') then + begin + case Text[2] of + 'L': Song.Language := Copy(Text, 3, Length(Text)); // language + 'T': begin // title info + if (Song.Artist = '') then + Song.Artist := Copy(Text, 3, Length(Text)) + else if (Song.Title = '') then + Song.Title := Copy(Text, 3, Length(Text)); + end; + end; + Continue; + end; + + LyricType := ltKMIDI; + end + else if (ltSMFLyric in Tracks[T].LyricType) and + (Tracks[T].Note[N].Data1 = MIDI_META_LYRICS) then + begin + LyricType := ltSMFLyric; + end + else + begin + // unknown meta event + Continue; end; + + AddLyric(Round(Tracks[T].Note[N].Start / Ticks), LyricType, Tracks[T].Note[N].Str); end; end; end; @@ -404,8 +491,12 @@ begin // copy notes SetLength(Lines.Line, 1); - Lines.Number := 1; - Lines.High := 0; + Lines.Number := 1; + Lines.High := 0; + Lines.Current := 0; + Lines.Resolution := 0; + Lines.NotesGAP := 0; + Lines.ScoreValue := 0; C := 0; N := 0; @@ -454,7 +545,6 @@ begin Lines.Line[C].Note[N].Length := Note[Nu].Len; Lines.Line[C].Note[N].Tone := Note[Nu].Tone; Lines.Line[C].Note[N].Text := DecodeStringUTF8(Note[Nu].Lyric, DEFAULT_ENCODING); - //All Notes are Freestyle when Converted Fix: Lines.Line[C].Note[N].NoteType := ntNormal; Inc(N); end; @@ -500,7 +590,7 @@ begin AddButton(500, 20, 100, 40, Skin.GetTextureFileName('ButtonF')); AddButtonText(20, 5, 0, 0, 0, 'Save'); - ConversionFileName := GamePath.Append('file.mid'); + fFileName := PATH_NONE; for P := 0 to 100 do begin @@ -515,84 +605,120 @@ procedure TScreenEditConvert.OnShow; var T: integer; // track N: integer; // note + {$IFDEF UseMIDIPort} + MidiTrack: TMidiTrack; + MidiEvent: PMidiEvent; + {$ENDIF} + FileOpened: boolean; + KMIDITrackIndex, SMFTrackIndex: integer; begin inherited; + Interaction := 0; + {$IFDEF UseMIDIPort} MidiOut := TMidiOutput.Create(nil); - //if Ini.Debug = 1 then - // MidiOut.ProductName := 'Microsoft GS Wavetable SW Synth'; // for my kxproject without midi table Log.LogInfo(MidiOut.ProductName, 'MIDI'); MidiOut.Open; MidiFile := nil; + SetLength(Tracks, 0); + + // Filename is only <> PATH_NONE if we called the OpenScreen before + fFilename := ScreenOpen.Filename; + if (fFilename = PATH_NONE) then + Exit; + ScreenOpen.Filename := PATH_NONE; - if ConversionFileName.Exists then + FileOpened := false; + if fFileName.Exists then begin MidiFile := TMidiFile.Create(nil); - MidiFile.Filename := ConversionFileName; - MidiFile.ReadFile; + MidiFile.Filename := fFileName; + try + MidiFile.ReadFile; + FileOpened := true; + except + MidiFile.Free; + end; + end; + + if (not FileOpened) then + begin + ScreenPopupError.ShowPopup(Language.Translate('ERROR_FILE_NOT_FOUND')); + Exit; + end; + + Len := 0; + SelTrack := 0; + BPM := MidiFile.Bpm; + Ticks := MidiFile.TicksPerQuarter / 4; - Len := 0; - SelTrack := 0; - BPM := MidiFile.Bpm; - Ticks := MidiFile.TicksPerQuarter / 4; + KMIDITrackIndex := -1; + SMFTrackIndex := -1; - SetLength(Tracks, MidiFile.NumberOfTracks); - for T := 0 to MidiFile.NumberOfTracks-1 do + SetLength(Tracks, MidiFile.NumberOfTracks); + for T := 0 to MidiFile.NumberOfTracks-1 do + Tracks[T].LyricType := []; + + for T := 0 to MidiFile.NumberOfTracks-1 do + begin + MidiTrack := MidiFile.GetTrack(T); + MidiTrack.OnMidiEvent := nil; + Tracks[T].Name := DecodeStringUTF8(MidiTrack.getName, DEFAULT_ENCODING); + Tracks[T].NoteType := ntNone; + Tracks[T].Status := []; + + SetLength(Tracks[T].Note, MidiTrack.getEventCount()); + for N := 0 to MidiTrack.getEventCount-1 do begin - MidiTrack := MidiFile.GetTrack(T); - MidiTrack.OnMidiEvent := nil; - Tracks[T].Name := MidiTrack.getName; - Tracks[T].NoteType := ntNone; - Tracks[T].LyricType := []; - Tracks[T].Status := []; - - SetLength(Tracks[T].Note, MidiTrack.getEventCount()); - for N := 0 to MidiTrack.getEventCount-1 do + MidiEvent := MidiTrack.GetEvent(N); + + Tracks[T].Note[N].Start := MidiEvent.time; + Tracks[T].Note[N].Len := MidiEvent.len; + Tracks[T].Note[N].Event := MidiEvent.event; + Tracks[T].Note[N].EventType := MidiEvent.event shr 4; + Tracks[T].Note[N].Channel := MidiEvent.event and $0F; + Tracks[T].Note[N].Data1 := MidiEvent.data1; + Tracks[T].Note[N].Data2 := MidiEvent.data2; + Tracks[T].Note[N].Str := DecodeStringUTF8(MidiEvent.str, DEFAULT_ENCODING); + + if (Tracks[T].Note[N].Event = MIDI_EVENT_META) then begin - MidiEvent := MidiTrack.GetEvent(N); - - Tracks[T].Note[N].Start := MidiEvent.time; - Tracks[T].Note[N].Len := MidiEvent.len; - Tracks[T].Note[N].Event := MidiEvent.event; - Tracks[T].Note[N].EventType := MidiEvent.event shr 4; - Tracks[T].Note[N].Channel := MidiEvent.event and $0F; - Tracks[T].Note[N].Data1 := MidiEvent.data1; - Tracks[T].Note[N].Data2 := MidiEvent.data2; - Tracks[T].Note[N].Str := MidiEvent.str; - - if (Tracks[T].Note[N].Event = MIDI_EVENT_META) then - begin - case (Tracks[T].Note[N].Data1) of - MIDI_META_TEXT: begin - if (Copy(Tracks[T].Note[N].Str, 1, 6) = '@KMIDI') then - begin - Tracks[T].LyricType := Tracks[T].LyricType + [ltKMIDI]; - //Tracks[T].Status := [tsLyrics]; - //DebugWriteln('Text: ' + Tracks[T].Note[N].Str); - end; - end; - MIDI_META_LYRICS: begin - // lyrics in Standard Midi File format found - Tracks[T].LyricType := Tracks[T].LyricType + [ltSMFLyric]; - //Tracks[T].Status := [tsLyrics]; + case (Tracks[T].Note[N].Data1) of + MIDI_META_TEXT: begin + // KMIDI lyrics (uses MIDI_META_TEXT events) + if (StrLComp(PAnsiChar(Tracks[T].Note[N].Str), '@KMIDI KARAOKE FILE', 19) = 0) and + (High(Tracks) >= T+1) then + begin + // The '@KMIDI ...' mark is in the first track (mostly named 'Soft Karaoke') + // but the lyrics are in the second track (named 'Words') + Tracks[T+1].LyricType := Tracks[T+1].LyricType + [ltKMIDI]; + KMIDITrackIndex := T+1; end; end; - end - else if (Tracks[T].Note[N].EventType = MIDI_EVENTTYPE_NOTEON) then - begin - // notes available - Tracks[T].NoteType := ntAvail; + MIDI_META_LYRICS: begin + // lyrics in Standard Midi File format found (uses MIDI_META_LYRICS events) + Tracks[T].LyricType := Tracks[T].LyricType + [ltSMFLyric]; + SMFTrackIndex := T; + end; end; - - if Tracks[T].Note[N].Start + Tracks[T].Note[N].Len > Len then - Len := Tracks[T].Note[N].Start + Tracks[T].Note[N].Len; + end + else if (Tracks[T].Note[N].EventType = MIDI_EVENTTYPE_NOTEON) then + begin + // notes available + Tracks[T].NoteType := ntAvail; end; - end; + if Tracks[T].Note[N].Start + Tracks[T].Note[N].Len > Len then + Len := Tracks[T].Note[N].Start + Tracks[T].Note[N].Len; + end; end; - Interaction := 0; + // set default lyric track. Prefer KMIDI. + if (KMIDITrackIndex > -1) then + Tracks[KMIDITrackIndex].Status := Tracks[KMIDITrackIndex].Status + [tsLyrics] + else if (SMFTrackIndex > -1) then + Tracks[SMFTrackIndex].Status := Tracks[SMFTrackIndex].Status + [tsLyrics]; {$ENDIF} end; @@ -658,12 +784,9 @@ begin for Count := 0 to High(Tracks) do begin - // track names should be ASCII only, but who knows - TrackName := DecodeStringUTF8(Tracks[Count].Name, DEFAULT_ENCODING); - SetFontPos(65, Y + Count*YSkip); SetFontSize(15); - glPrint(TrackName); + glPrint(Tracks[Count].Name); end; for Count := 0 to High(Tracks) do @@ -684,7 +807,8 @@ begin // playing line {$IFDEF UseMIDIPort} - X := 60 + MidiFile.GetCurrentTime/MidiFile.GetTrackLength*730; + if (MidiFile <> nil) then + X := 60 + MidiFile.GetCurrentTime/MidiFile.GetTrackLength*730; {$ENDIF} DrawLine(X, Y, X, Bottom, 0.3, 0.3, 0.3); @@ -694,9 +818,9 @@ end; procedure TScreenEditConvert.OnHide; begin {$IFDEF UseMIDIPort} - MidiFile.Free; + FreeAndNil(MidiFile); MidiOut.Close; - MidiOut.Free; + FreeAndNil(MidiOut); {$ENDIF} end; -- cgit v1.2.3