{* 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
*}
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: AnsiString;
end;
TTrack = record
Note: array of TMidiNote;
Name: AnsiString;
Status: set of (tsNotes, tsLyrics);
LyricType: set of (ltKMIDI, ltSMFLyric);
NoteType: (ntNone, ntAvail);
end;
TNote = record
Start: integer;
Len: integer;
Tone: integer;
Lyric: AnsiString;
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
//FileName: 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;
{$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;
var
ConversionFileName: IPath;
implementation
uses
SysUtils,
TextGL,
gl,
UDrawTexture,
UFiles,
UGraphic,
UIni,
UMain,
UPathUtils,
USkins,
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;
var
SResult: TSaveSongResult;
Playing: boolean;
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}
MidiFile.StopPlaying;
{$ENDIF}
AudioPlayback.PlaySound(SoundLib.Back);
FadeTo(@ScreenEdit);
end;
SDLK_RETURN:
begin
if Interaction = 0 then
begin
AudioPlayback.PlaySound(SoundLib.Start);
ScreenOpen.BackScreen := @ScreenEditConvert;
FadeTo(@ScreenOpen);
end;
if Interaction = 1 then
begin
{$IFDEF UseMIDIPort}
MidiFile.OnMidiEvent := MidiFile1MidiEvent;
//MidiFile.GoToTime(MidiFile.GetTrackLength div 2);
MidiFile.StartPlaying;
{$ENDIF}
end;
if Interaction = 2 then
begin
{$IFDEF UseMIDIPort}
MidiFile.OnMidiEvent := nil;
MidiFile.StartPlaying;
{$ENDIF}
end;
if Interaction = 3 then
begin
if CountSelectedTracks > 0 then
begin
Extract;
SResult := SaveSong(Song, Lines, ConversionFileName.SetExtension('.txt'),
false);
if (SResult <> ssrOK) then
begin
ScreenPopupError.ShowPopup('Could not save file');
end;
end;
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
begin
if (Tracks[SelTrack].Status = []) then
Tracks[SelTrack].Status := [tsLyrics]
else
Tracks[SelTrack].Status := [];
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;
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; Text: AnsiString);
var
N: integer;
begin
for N := 0 to 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;
Exit;
end;
end;
DebugWriteln('Missing: ' + Text);
end;
procedure TScreenEditConvert.Extract;
var
T: integer;
C: integer;
N: integer;
Nu: integer;
NoteTemp: TNote;
Move: integer;
Max, Min: integer;
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
for T := 0 to High(Tracks) do
begin
if tsLyrics in Tracks[T].Status then
begin
for N := 0 to High(Tracks[T].Note) do
begin
if (Tracks[T].Note[N].Event = MIDI_EVENT_META) and
(Tracks[T].Note[N].Data1 = MIDI_META_LYRICS) then
begin
AddLyric(Round(Tracks[T].Note[N].Start / Ticks), Tracks[T].Note[N].Str);
end;
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;
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);
//All Notes are Freestyle when Converted Fix:
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');
ConversionFileName := GamePath.Append('file.mid');
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;
var
T: integer; // track
N: integer; // note
begin
inherited;
{$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;
if ConversionFileName.Exists then
begin
MidiFile := TMidiFile.Create(nil);
MidiFile.Filename := ConversionFileName;
MidiFile.ReadFile;
Len := 0;
SelTrack := 0;
BPM := MidiFile.Bpm;
Ticks := MidiFile.TicksPerQuarter / 4;
SetLength(Tracks, MidiFile.NumberOfTracks);
for T := 0 to MidiFile.NumberOfTracks-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
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];
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;
end;
Interaction := 0;
{$ENDIF}
end;
function TScreenEditConvert.Draw: boolean;
var
Count: integer;
Count2: integer;
Bottom: real;
X: real;
Y: real;
Height: real;
YSkip: real;
TrackName: UTF8String;
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
// 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);
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}
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}
MidiFile.Free;
MidiOut.Close;
MidiOut.Free;
{$ENDIF}
end;
end.