{* UltraStar Deluxe - Karaoke Game * * UltraStar Deluxe is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * $URL$ * $Id$ *} unit UScreenEditConvert; {* * See * MIDI Recommended Practice (RP-017): SMF Lyric Meta Event Definition * http://www.midi.org/techspecs/rp17.php * MIDI Recommended Practice (RP-026): SMF Language and Display Extensions * 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 {$IFDEF FPC} {$MODE Delphi} {$ENDIF} {$I switches.inc} uses math, UMenu, SDL, {$IFDEF UseMIDIPort} MidiFile, MidiOut, {$ENDIF} ULog, USongs, USong, UMusic, UThemes, UPath; type TMidiNote = record Event: integer; EventType: integer; Channel: integer; Start: real; Len: real; Data1: integer; Data2: integer; Str: UTF8String; // normally ASCII end; TLyricType = (ltKMIDI, ltSMFLyric); TTrack = record Note: array of TMidiNote; Name: UTF8String; // normally ASCII Status: set of (tsNotes, tsLyrics); //< track contains notes, lyrics or both LyricType: set of TLyricType; NoteType: (ntNone, ntAvail); end; TNote = record Start: integer; Len: integer; Tone: integer; Lyric: UTF8String; NewSentence: boolean; end; TArrayTrack = array of TTrack; TScreenEditConvert = class(TMenu) private Tracks: TArrayTrack; // current track ColR: array[0..100] of real; ColG: array[0..100] of real; ColB: array[0..100] of real; Len: real; SelTrack: integer; // index of selected track fFileName: IPath; {$IFDEF UseMIDIPort} MidiFile: TMidiFile; MidiOut: TMidiOutput; {$ENDIF} BPM: real; Ticks: real; Note: array of TNote; procedure AddLyric(Start: integer; LyricType: TLyricType; Text: UTF8String); procedure Extract(out Song: TSong; out Lines: TLines); {$IFDEF UseMIDIPort} procedure MidiFile1MidiEvent(event: PMidiEvent); {$ENDIF} function CountSelectedTracks: integer; public constructor Create; override; procedure OnShow; override; function ParseInput(PressedKey: cardinal; CharCode: UCS4Char; PressedDown: boolean): boolean; override; function Draw: boolean; override; procedure OnHide; override; end; implementation uses SysUtils, TextGL, gl, UDrawTexture, UFiles, UGraphic, UIni, UMain, UPathUtils, USkins, ULanguage, UTextEncoding, UUnicodeUtils; const // MIDI/KAR lyrics are specified to be ASCII only. // Assume backward compatible CP1252 encoding. DEFAULT_ENCODING = encCP1252; const MIDI_EVENTTYPE_NOTEOFF = $8; MIDI_EVENTTYPE_NOTEON = $9; MIDI_EVENTTYPE_META_SYSEX = $F; MIDI_EVENT_META = $FF; MIDI_META_TEXT = $1; 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 begin // Key Down // check normal keys case UCS4UpperCase(CharCode) of Ord('Q'): begin Result := false; Exit; end; end; // check special keys case PressedKey of SDLK_ESCAPE, SDLK_BACKSPACE : begin {$IFDEF UseMIDIPort} if (MidiFile <> nil) then MidiFile.StopPlaying; {$ENDIF} AudioPlayback.PlaySound(SoundLib.Back); FadeTo(@ScreenEdit); end; SDLK_RETURN: begin if Interaction = 0 then begin AudioPlayback.PlaySound(SoundLib.Start); ScreenOpen.Filename := GamePath.Append('file.mid'); ScreenOpen.BackScreen := @ScreenEditConvert; FadeTo(@ScreenOpen); end else if Interaction = 1 then begin {$IFDEF UseMIDIPort} if (MidiFile <> nil) then begin MidiFile.OnMidiEvent := MidiFile1MidiEvent; //MidiFile.GoToTime(MidiFile.GetTrackLength div 2); MidiFile.StartPlaying; end; {$ENDIF} end else if Interaction = 2 then begin {$IFDEF UseMIDIPort} if (MidiFile <> nil) then begin MidiFile.OnMidiEvent := nil; MidiFile.StartPlaying; end; {$ENDIF} end else if Interaction = 3 then begin {$IFDEF UseMIDIPort} if CountSelectedTracks > 0 then begin Extract(Song, Lines); SResult := SaveSong(Song, Lines, fFileName.SetExtension('.txt'), false); 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('EDITOR_ERROR_NO_TRACK_SELECTED')); end; {$ENDIF} end; end; SDLK_SPACE: begin {$IFDEF UseMIDIPort} if (MidiFile <> nil) then 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 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 MidiTrack.OnMidiEvent := nil; if (Playing) then MidiFile.ContinuePlaying(); end; {$ENDIF} end; SDLK_RIGHT: begin InteractNext; end; SDLK_LEFT: begin InteractPrev; end; SDLK_DOWN: begin Inc(SelTrack); if SelTrack > High(Tracks) then SelTrack := 0; end; SDLK_UP: begin Dec(SelTrack); if SelTrack < 0 then SelTrack := High(Tracks); end; end; end; end; procedure TScreenEditConvert.AddLyric(Start: integer; LyricType: TLyricType; Text: UTF8String); var N: integer; begin // find corresponding note N := 0; while (N <= High(Note)) do begin if Note[N].Start = Start then Break; Inc(N); end; // 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; // 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(out Song: TSong; out Lines: TLines); var T: integer; C: integer; N: integer; Nu: integer; NoteTemp: TNote; Move: integer; Max, Min: integer; LyricType: TLyricType; Text: UTF8String; begin // song info Song := TSong.Create(); Song.Clear(); Song.Resolution := 4; SetLength(Song.BPM, 1); Song.BPM[0].BPM := BPM*4; SetLength(Note, 0); // extract notes for T := 0 to High(Tracks) do begin if tsNotes in Tracks[T].Status then begin for N := 0 to High(Tracks[T].Note) do begin if (Tracks[T].Note[N].EventType = MIDI_EVENTTYPE_NOTEON) and (Tracks[T].Note[N].Data2 > 0) then begin Nu := Length(Note); SetLength(Note, Nu + 1); Note[Nu].Start := Round(Tracks[T].Note[N].Start / Ticks); Note[Nu].Len := Round(Tracks[T].Note[N].Len / Ticks); Note[Nu].Tone := Tracks[T].Note[N].Data1 - 12*5; Note[Nu].Lyric := '-'; end; end; end; end; // extract lyrics (and artist + title info) for T := 0 to High(Tracks) do begin if not (tsLyrics in Tracks[T].Status) then Continue; for N := 0 to High(Tracks[T].Note) do begin if (Tracks[T].Note[N].Event = MIDI_EVENT_META) then begin // determine and validate lyric meta tag if (ltKMIDI in Tracks[T].LyricType) and (Tracks[T].Note[N].Data1 = MIDI_META_TEXT) then begin 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; // sort notes for N := 0 to High(Note) do for Nu := 0 to High(Note)-1 do if Note[Nu].Start > Note[Nu+1].Start then begin NoteTemp := Note[Nu]; Note[Nu] := Note[Nu+1]; Note[Nu+1] := NoteTemp; end; // move to 0 at beginning Move := Note[0].Start; for N := 0 to High(Note) do Note[N].Start := Note[N].Start - Move; // copy notes SetLength(Lines.Line, 1); Lines.Number := 1; Lines.High := 0; Lines.Current := 0; Lines.Resolution := 0; Lines.NotesGAP := 0; Lines.ScoreValue := 0; C := 0; N := 0; Lines.Line[C].HighNote := -1; for Nu := 0 to High(Note) do begin if Note[Nu].NewSentence then // new line begin SetLength(Lines.Line, Length(Lines.Line)+1); Lines.Number := Lines.Number + 1; Lines.High := Lines.High + 1; C := C + 1; N := 0; SetLength(Lines.Line[C].Note, 0); Lines.Line[C].HighNote := -1; //Calculate Start of the Last Sentence if (C > 0) and (Nu > 0) then begin Max := Note[Nu].Start; Min := Note[Nu-1].Start + Note[Nu-1].Len; case (Max - Min) of 0: Lines.Line[C].Start := Max; 1: Lines.Line[C].Start := Max; 2: Lines.Line[C].Start := Max - 1; 3: Lines.Line[C].Start := Max - 2; else if ((Max - Min) > 4) then Lines.Line[C].Start := Min + 2 else Lines.Line[C].Start := Max; end; // case end; end; // create space for new note SetLength(Lines.Line[C].Note, Length(Lines.Line[C].Note)+1); Inc(Lines.Line[C].HighNote); // initialize note Lines.Line[C].Note[N].Start := Note[Nu].Start; 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); Lines.Line[C].Note[N].NoteType := ntNormal; Inc(N); end; end; function TScreenEditConvert.CountSelectedTracks: integer; var T: integer; // track begin Result := 0; for T := 0 to High(Tracks) do if tsNotes in Tracks[T].Status then Inc(Result); end; {$IFDEF UseMIDIPort} procedure TScreenEditConvert.MidiFile1MidiEvent(event: PMidiEvent); begin //Log.LogStatus(IntToStr(event.event), 'MIDI'); try MidiOut.PutShort(event.event, event.data1, event.data2); except MidiFile.StopPlaying(); end; end; {$ENDIF} constructor TScreenEditConvert.Create; var P: integer; begin inherited Create; AddButton(40, 20, 100, 40, Skin.GetTextureFileName('ButtonF')); AddButtonText(15, 5, 0, 0, 0, 'Open'); //Button[High(Button)].Text[0].Size := 11; AddButton(160, 20, 100, 40, Skin.GetTextureFileName('ButtonF')); AddButtonText(25, 5, 0, 0, 0, 'Play'); AddButton(280, 20, 200, 40, Skin.GetTextureFileName('ButtonF')); AddButtonText(25, 5, 0, 0, 0, 'Play Selected'); AddButton(500, 20, 100, 40, Skin.GetTextureFileName('ButtonF')); AddButtonText(20, 5, 0, 0, 0, 'Save'); fFileName := PATH_NONE; for P := 0 to 100 do begin ColR[P] := Random(10)/10; ColG[P] := Random(10)/10; ColB[P] := Random(10)/10; end; end; procedure TScreenEditConvert.OnShow; {$IFDEF UseMIDIPort} var T: integer; // track N: integer; // note MidiTrack: TMidiTrack; MidiEvent: PMidiEvent; FileOpened: boolean; KMIDITrackIndex, SMFTrackIndex: integer; {$ENDIF} begin inherited; Interaction := 0; {$IFDEF UseMIDIPort} MidiOut := TMidiOutput.Create(nil); 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; FileOpened := false; if fFileName.Exists then begin MidiFile := TMidiFile.Create(nil); 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; KMIDITrackIndex := -1; SMFTrackIndex := -1; 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 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 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; 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; end else if (Tracks[T].Note[N].EventType = MIDI_EVENTTYPE_NOTEON) then begin // notes available Tracks[T].NoteType := ntAvail; 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; // 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; function TScreenEditConvert.Draw: boolean; var Count: integer; Count2: integer; Bottom: real; X: real; Y: real; Height: real; YSkip: real; begin // draw static menu inherited Draw; Y := 100; Height := min(480, 40 * Length(Tracks)); Bottom := Y + Height; YSkip := Height / Length(Tracks); // highlight selected track DrawQuad(10, Y+SelTrack*YSkip, 780, YSkip, 0.8, 0.8, 0.8); // track-selection info for Count := 0 to High(Tracks) do if Tracks[Count].Status <> [] then DrawQuad(10, Y + Count*YSkip, 50, YSkip, 0.8, 0.3, 0.3); glColor3f(0, 0, 0); for Count := 0 to High(Tracks) do begin if Tracks[Count].NoteType = ntAvail then begin if tsNotes in Tracks[Count].Status then glColor3f(0, 0, 0) else glColor3f(0.7, 0.7, 0.7); SetFontPos(25, Y + Count*YSkip + 10); SetFontSize(15); glPrint('N'); end; if Tracks[Count].LyricType <> [] then begin if tsLyrics in Tracks[Count].Status then glColor3f(0, 0, 0) else glColor3f(0.7, 0.7, 0.7); SetFontPos(40, Y + Count*YSkip + 10); SetFontSize(15); glPrint('L'); end; end; DrawLine( 10, Y, 10, Bottom, 0, 0, 0); DrawLine( 60, Y, 60, Bottom, 0, 0, 0); DrawLine(790, Y, 790, Bottom, 0, 0, 0); for Count := 0 to Length(Tracks) do DrawLine(10, Y + Count*YSkip, 790, Y + Count*YSkip, 0, 0, 0); for Count := 0 to High(Tracks) do begin SetFontPos(65, Y + Count*YSkip); SetFontSize(15); glPrint(Tracks[Count].Name); end; for Count := 0 to High(Tracks) do begin for Count2 := 0 to High(Tracks[Count].Note) do begin if Tracks[Count].Note[Count2].EventType = MIDI_EVENTTYPE_NOTEON then DrawQuad(60 + Tracks[Count].Note[Count2].Start/Len * 725, Y + (Count+1)*YSkip - Tracks[Count].Note[Count2].Data1*35/127, 3, 3, ColR[Count], ColG[Count], ColB[Count]); if Tracks[Count].Note[Count2].EventType = 15 then DrawLine(60 + Tracks[Count].Note[Count2].Start/Len * 725, Y + 0.75 * YSkip + Count*YSkip, 60 + Tracks[Count].Note[Count2].Start/Len * 725, Y + YSkip + Count*YSkip, ColR[Count], ColG[Count], ColB[Count]); end; end; // playing line {$IFDEF UseMIDIPort} if (MidiFile <> nil) then X := 60 + MidiFile.GetCurrentTime/MidiFile.GetTrackLength*730; {$ENDIF} DrawLine(X, Y, X, Bottom, 0.3, 0.3, 0.3); Result := true; end; procedure TScreenEditConvert.OnHide; begin {$IFDEF UseMIDIPort} FreeAndNil(MidiFile); MidiOut.Close; FreeAndNil(MidiOut); {$ENDIF} end; end.