From 82b9fcd1dd88208cdc8f67dd054c6386e2ae8baf Mon Sep 17 00:00:00 2001 From: tbgitoo Date: Sat, 2 Oct 2021 22:02:50 +0200 Subject: [PATCH 1/6] Language strings and theme options --- game/languages/English.ini | 13 + game/themes/Deluxe.ini | 455 ++++++++++++++++++++++ game/themes/Deluxe/Blue.ini | 4 + game/themes/Deluxe/Fall.ini | 4 + game/themes/Deluxe/Ocean.ini | 4 + game/themes/Deluxe/Ribbon.ini | 4 + game/themes/Deluxe/Summer.ini | 4 + game/themes/Deluxe/Winter.ini | 4 + game/themes/Deluxe/[editsub]beat-note.png | Bin 0 -> 1179 bytes game/themes/Deluxe/[sing]clap.png | Bin 0 -> 412 bytes game/themes/Deluxe/[sing]notesBeat.png | Bin 0 -> 538 bytes game/themes/Deluxe/[sing]notesBeatBG.png | Bin 0 -> 402 bytes game/themes/Modern.ini | 445 +++++++++++++++++++++ game/themes/Modern/Blue.ini | 4 + game/themes/Modern/Winter.ini | 4 + game/themes/Modern/[editsub]beat-note.png | Bin 0 -> 1179 bytes game/themes/Modern/[sing]clap.png | Bin 0 -> 412 bytes game/themes/Modern/[sing]notesBeat.png | Bin 0 -> 538 bytes game/themes/Modern/[sing]notesBeatBG.png | Bin 0 -> 402 bytes src/base/UThemes.pas | 47 ++- 20 files changed, 991 insertions(+), 1 deletion(-) create mode 100644 game/themes/Deluxe/[editsub]beat-note.png create mode 100644 game/themes/Deluxe/[sing]clap.png create mode 100644 game/themes/Deluxe/[sing]notesBeat.png create mode 100644 game/themes/Deluxe/[sing]notesBeatBG.png create mode 100644 game/themes/Modern/[editsub]beat-note.png create mode 100644 game/themes/Modern/[sing]clap.png create mode 100644 game/themes/Modern/[sing]notesBeat.png create mode 100644 game/themes/Modern/[sing]notesBeatBG.png diff --git a/game/languages/English.ini b/game/languages/English.ini index 2692786bd..709d4dc9a 100644 --- a/game/languages/English.ini +++ b/game/languages/English.ini @@ -824,6 +824,19 @@ SING_OPTIONS_JUKEBOX=Jukebox SING_OPTIONS_JUKEBOX_DESC=Jukebox configuration SING_OPTIONS_JUKEBOX_WHEREAMI=Tools » Options » Jukebox +SING_OPTIONS_BEATPLAY_SHOW_CLAP=Clap icon +SING_OPTIONS_BEATPLAY_AUDIO_CONFIGURE=Configure Audio +SING_OPTIONS_BEAT_PLAYING=Beat Tapping +SING_OPTIONS_BEATPLAY_BEATDETECTION_DELAY=Beat detection delay +SING_OPTIONS_BEATPLAY_SHOW_CLAP=Clap icon + +BEATDETECT_OPTIONS_RECORD_DESC=Tap Audio Configuration +BEATDETECT_OPTIONS_RECORD_WHEREAMI=Tools » Options » Tap » Audio +BEATDETECT_OPTIONS_SOUND_THRESHOLD=Threshold + +BEATPLAY_OPTIONS_RECORD_DESC=Tapping +BEATPLAY_OPTIONS_RECORD_WHEREAMI=Tools » Options » Tap + OPTION_VALUE_TO_SING=Sing OPTION_VALUE_ACTUAL=Actual OPTION_VALUE_NEXT=Next diff --git a/game/themes/Deluxe.ini b/game/themes/Deluxe.ini index d879cae1b..c089a1a41 100644 --- a/game/themes/Deluxe.ini +++ b/game/themes/Deluxe.ini @@ -3005,6 +3005,35 @@ Align = 1 Text = SING_OPTIONS_JUKEBOX Color = White + +[OptionsButtonBeatPlaying] +X = 560 +Y = 385 +W = 150 +H = 50 +Tex = Button +Color = ColorLight +DColor = ColorDark +Type = Transparent +Align = 0 +Texts = 1 +Fade = 1 +FadeText = 1 +SelectH = 100 +FadeTex = ButtonFade +FadeTexPos = 0 + +[OptionsButtonBeatPlayingText1] +X = 75 +Y = 10 +Font = 0 +Style = 0 +Size = 30 +Align = 1 +Text = SING_OPTIONS_BEAT_PLAYING +Color = White + + [OptionsButtonExit] X = 590 Y = 490 @@ -4925,6 +4954,421 @@ Type = Transparent Reflection = 1 ReflectionSpacing = 2 + + +[OptionsBeatPlay] +Texts = 5 + +[OptionsBeatPlayBackground] +Tex = OptionsBG + +[OptionsBeatPlayStatic1] +X = 40 +Y = 22 +W = 25 +H = 23 +Color = White +Tex = IconOption +Type = Transparent + +[OptionsBeatPlayText1] +X = 70 +Y = 5 +Color = White +Size = 54 +Text = SING_OPTIONS + +[OptionsBeatPlayText2] +X = 70 +Y = 60 +Color = ColorLightest +Size = 30 +Text = BEATPLAY_OPTIONS_RECORD_DESC + +[OptionsBeatPlayText3] +X = 238 +Y = 550 +Color = ColorLightest +Font = 0 +Style = 0 +Size = 18 +Align = 2 +Text = BEATPLAY_OPTIONS_RECORD_WHEREAMI + +[OptionsBeatPlayStatic2] +X = 0 +Y = 545 +W = 250 +H = 30 +Tex = Leiste1 +Color = ColorLight +Type = Colorized +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatPlayStatic3] +X = 250 +Y = 545 +W = 550 +H = 30 +Tex = Leiste2 +Color = White +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatPlayStatic4] +X = 260 +Y = 545 +W = 32 +H = 30 +Tex = ButtonNavi +Color = White +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatPlayText4] +X = 300 +Y = 548 +Z = 0.5 +Color = Black +Size = 24 +Reflection = 1 +ReflectionSpacing = 20 +Text = SING_LEGEND_NAVIGATE + +[OptionsBeatPlayStatic5] +X = 400 +Y = 545 +W = 32 +H = 30 +Tex = ButtonEsc +Color = White +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatPlayText5] +X = 440 +Y = 548 +Z = 0.5 +Color = Black +Size = 24 +Reflection = 1 +ReflectionSpacing = 20 +Text = SING_LEGEND_ESC + + +[OptionsBeatPlaySelectBeatDetectionDelay] +Text = SING_OPTIONS_BEATPLAY_BEATDETECTION_DELAY +Tex = MainBar +Type = Transparent +TexSBG = SelectBG +TypeSBG = Transparent +X = 70 +Y = 175 +W = 250 +H = 35 +SkipX = 10 + +DColor = ColorDark +Color = ColorLight +TColor = White +TDColor = White + +SBGDColor = ColorDark +SBGColor = ColorLight +STColor = White +STDColor = GrayDark + + +[OptionsBeatPlayShowClap] +Text = SING_OPTIONS_BEATPLAY_SHOW_CLAP +Tex = MainBar +Type = Transparent +TexSBG = SelectBG +TypeSBG = Transparent +X = 70 +Y = 310 +W = 250 +H = 35 +SkipX = 10 + +DColor = ColorDark +Color = ColorLight +TColor = White +TDColor = White + +SBGDColor = ColorDark +SBGColor = ColorLight +STColor = White +STDColor = GrayDark + + + + +[OptionsBeatPlayButtonExit] +X = 70 +Y = 490 +W = 250 +H = 40 +Tex = Button +Color = ColorLight +DColor = ColorDark +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatPlayButtonAudioConfigure] +X = 500 +Y = 490 +W = 250 +H = 40 +Tex = Button +Color = ColorLight +DColor = ColorDark +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 +Text = SING_OPTIONS_KEYPLAY_AUDIO_CONFIGURE + + +[OptionsBeatDetect] +Texts = 5 + +[OptionsBeatDetectBackground] +Tex = OptionsBG + +[OptionsBeatDetectStatic1] +X = 40 +Y = 22 +W = 25 +H = 23 +Color = White +Tex = IconOption +Type = Transparent + +[OptionsBeatDetectText1] +X = 70 +Y = 5 +Color = White +Size = 54 +Text = SING_OPTIONS + +[OptionsBeatDetectText2] +X = 70 +Y = 60 +Color = ColorLightest +Size = 30 +Text = BEATDETECT_OPTIONS_RECORD_DESC + +[OptionsBeatDetectText3] +X = 200 +Y = 550 +Color = ColorLightest +Font = 0 +Style = 0 +Size = 18 +Align = 2 +Text = BEATDETECT_OPTIONS_RECORD_WHEREAMI + +[OptionsBeatDetectStatic2] +X = 0 +Y = 545 +W = 250 +H = 30 +Tex = Leiste1 +Color = ColorLight +Type = Colorized +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatDetectStatic3] +X = 250 +Y = 545 +W = 550 +H = 30 +Tex = Leiste2 +Color = White +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatDetectStatic4] +X = 260 +Y = 545 +W = 32 +H = 30 +Tex = ButtonNavi +Color = White +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatDetectText4] +X = 300 +Y = 548 +Z = 0.5 +Color = Black +Size = 24 +Reflection = 1 +ReflectionSpacing = 20 +Text = SING_LEGEND_NAVIGATE + +[OptionsBeatDetectStatic5] +X = 400 +Y = 545 +W = 32 +H = 30 +Tex = ButtonEsc +Color = White +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 + +[OptionsBeatDetectText5] +X = 440 +Y = 548 +Z = 0.5 +Color = Black +Size = 24 +Reflection = 1 +ReflectionSpacing = 20 +Text = SING_LEGEND_ESC + + +[OptionsBeatDetectSelectSlideCard] +Text = SING_OPTIONS_RECORD_CARD +Tex = MainBar +Type = Transparent +TexSBG = SelectBG +TypeSBG = Transparent +X = 70 +Y = 130 +W = 250 +H = 40 +SkipX = 10 + +DColor = ColorDark +Color = ColorLight +TColor = White +TDColor = White + +SBGDColor = ColorDark +SBGColor = ColorLight +STColor = White +STDColor = GrayDark + +[OptionsBeatDetectSelectSlideInput] +Text = SING_OPTIONS_RECORD_INPUT +Tex = MainBar +Type = Transparent +TexSBG = SelectBG +TypeSBG = Transparent +X = 70 +Y = 180 +W = 250 +H = 40 +SkipX = 10 + +DColor = ColorDark +Color = ColorLight +TColor = White +TDColor = White + +SBGDColor = ColorDark +SBGColor = ColorLight +STColor = White +STDColor = GrayDark + +[OptionsBeatDetectSelectChannel] +Text = SING_OPTIONS_RECORD_CHANNEL +Tex = MainBar +Type = Transparent +TexSBG = SelectBG +TypeSBG = Transparent +X = 70 +Y = 230 +W = 250 +H = 40 +SkipX = 10 + +DColor = ColorDark +Color = ColorLight +TColor = White +TDColor = White + +SBGDColor = ColorDark +SBGColor = ColorLight +STColor = White +STDColor = GrayDark + +[OptionsBeatDetectSelectThreshold] +Tex = MainBar +Type = Transparent +TexSBG = SelectBG +TypeSBG = Transparent +Text = BEATDETECT_OPTIONS_SOUND_THRESHOLD +X = 70 +Y = 280 +W = 250 +H = 40 +SkipX = 10 + +DColor = ColorDark +Color = ColorLight +TColor = White +TDColor = White + +SBGDColor = ColorDark +SBGColor = ColorLight +STColor = White +STDColor = GrayDark + + +[OptionsBeatDetectSelectMicBoost] +Tex = MainBar +Type = Transparent +TexSBG = SelectBG +TypeSBG = Transparent +Text = SING_OPTIONS_SOUND_MIC_BOOST +X = 70 +Y = 330 +W = 250 +H = 40 +SkipX = 10 + +DColor = ColorDark +Color = ColorLight +TColor = White +TDColor = White + +SBGDColor = ColorDark +SBGColor = ColorLight +STColor = White +STDColor = GrayDark + + + + + +[OptionsBeatDetectButtonExit] +X = 70 +Y = 490 +W = 250 +H = 40 +Tex = Button +Color = ColorLight +DColor = ColorDark +Type = Transparent +Reflection = 1 +ReflectionSpacing = 2 + + + + + [OptionsNetwork] Texts = 5 @@ -14865,6 +15309,17 @@ Color = ColorLight Reflection = 0 ReflectionSpacing = 31 +[EditSubBarStatic9] +Tex = beatnote +X = 380 +Y = 260 +W = 18 +H = 18 +Type = Transparent +Color = ColorLight +Reflection = 0 +ReflectionSpacing = 31 + ################################## ########## Duet ########## ################################## diff --git a/game/themes/Deluxe/Blue.ini b/game/themes/Deluxe/Blue.ini index 049f198ba..210ffdcf8 100644 --- a/game/themes/Deluxe/Blue.ini +++ b/game/themes/Deluxe/Blue.ini @@ -217,6 +217,8 @@ GrayLeft = [sing]notesLeft.png GrayMid = [sing]notesMid.png GrayRight = [sing]notesRight.png # unsung notes - colorized with playercolors +NoteBeatBG = [sing]notesBeatBG.png +NoteBeat = [sing]notesBeat.png NotePlainLeft = [sing]notesPlainLeft.png NotePlainMid = [sing]notesPlainMid.png NotePlainRight = [sing]notesPlainRight.png @@ -225,6 +227,7 @@ NoteBGLeft = [sing]notesBgLeft.png NoteBGMid = [sing]notesBgMid.png NoteBGRight = [sing]notesBgRight.png Pause = [sing]pause.png +BeatClap = [sing]clap.png GrayLeftRap = [rap]notesLeft.png GrayMidRap = [rap]notesMid.png @@ -296,6 +299,7 @@ previousseq = [editsub]previous-seq.png undo = [editsub]undo.png gold = [editsub]gold.png freestyle = [editsub]freestyle.png +beatnote = [editsub]beat-note.png # # # COLOR PICKER # # # PickerBG = [color]pickerbg.png diff --git a/game/themes/Deluxe/Fall.ini b/game/themes/Deluxe/Fall.ini index beb9f885a..22145a24e 100644 --- a/game/themes/Deluxe/Fall.ini +++ b/game/themes/Deluxe/Fall.ini @@ -217,6 +217,8 @@ GrayLeft = [sing]notesLeft.png GrayMid = [sing]notesMid.png GrayRight = [sing]notesRight.png # unsung notes - colorized with playercolors +NoteBeatBG = [sing]notesBeatBG.png +NoteBeat = [sing]notesBeat.png NotePlainLeft = [sing]notesPlainLeft.png NotePlainMid = [sing]notesPlainMid.png NotePlainRight = [sing]notesPlainRight.png @@ -225,6 +227,7 @@ NoteBGLeft = [sing]notesBgLeft.png NoteBGMid = [sing]notesBgMid.png NoteBGRight = [sing]notesBgRight.png Pause = [sing]pause.png +BeatClap = [sing]clap.png GrayLeftRap = [rap]notesLeft.png GrayMidRap = [rap]notesMid.png @@ -296,6 +299,7 @@ previousseq = [editsub]previous-seq.png undo = [editsub]undo.png gold = [editsub]gold.png freestyle = [editsub]freestyle.png +beatnote = [editsub]beat-note.png # # # COLOR PICKER # # # PickerBG = [color]pickerbg.png diff --git a/game/themes/Deluxe/Ocean.ini b/game/themes/Deluxe/Ocean.ini index 3bafc8c05..232eece35 100644 --- a/game/themes/Deluxe/Ocean.ini +++ b/game/themes/Deluxe/Ocean.ini @@ -217,6 +217,8 @@ GrayLeft = [sing]notesLeft.png GrayMid = [sing]notesMid.png GrayRight = [sing]notesRight.png # unsung notes - colorized with playercolors +NoteBeatBG = [sing]notesBeatBG.png +NoteBeat = [sing]notesBeat.png NotePlainLeft = [sing]notesPlainLeft.png NotePlainMid = [sing]notesPlainMid.png NotePlainRight = [sing]notesPlainRight.png @@ -225,6 +227,7 @@ NoteBGLeft = [sing]notesBgLeft.png NoteBGMid = [sing]notesBgMid.png NoteBGRight = [sing]notesBgRight.png Pause = [sing]pause.png +BeatClap = [sing]clap.png GrayLeftRap = [rap]notesLeft.png GrayMidRap = [rap]notesMid.png @@ -297,6 +300,7 @@ previousseq = [editsub]previous-seq.png undo = [editsub]undo.png gold = [editsub]gold.png freestyle = [editsub]freestyle.png +beatnote = [editsub]beat-note.png # # # COLOR PICKER # # # PickerBG = [color]pickerbg.png diff --git a/game/themes/Deluxe/Ribbon.ini b/game/themes/Deluxe/Ribbon.ini index 57b806900..c6989ffa9 100644 --- a/game/themes/Deluxe/Ribbon.ini +++ b/game/themes/Deluxe/Ribbon.ini @@ -217,6 +217,8 @@ GrayLeft = [sing]notesLeft.png GrayMid = [sing]notesMid.png GrayRight = [sing]notesRight.png # unsung notes - colorized with playercolors +NoteBeatBG = [sing]notesBeatBG.png +NoteBeat = [sing]notesBeat.png NotePlainLeft = [sing]notesPlainLeft.png NotePlainMid = [sing]notesPlainMid.png NotePlainRight = [sing]notesPlainRight.png @@ -225,6 +227,7 @@ NoteBGLeft = [sing]notesBgLeft.png NoteBGMid = [sing]notesBgMid.png NoteBGRight = [sing]notesBgRight.png Pause = [sing]pause.png +BeatClap = [sing]clap.png GrayLeftRap = [rap]notesLeft.png GrayMidRap = [rap]notesMid.png @@ -297,6 +300,7 @@ previousseq = [editsub]previous-seq.png undo = [editsub]undo.png gold = [editsub]gold.png freestyle = [editsub]freestyle.png +beatnote = [editsub]beat-note.png # # # COLOR PICKER # # # PickerBG = [color]pickerbg.png diff --git a/game/themes/Deluxe/Summer.ini b/game/themes/Deluxe/Summer.ini index 01b41410d..6cbf96d0b 100644 --- a/game/themes/Deluxe/Summer.ini +++ b/game/themes/Deluxe/Summer.ini @@ -217,6 +217,8 @@ GrayLeft = [sing]notesLeft.png GrayMid = [sing]notesMid.png GrayRight = [sing]notesRight.png # unsung notes - colorized with playercolors +NoteBeatBG = [sing]notesBeatBG.png +NoteBeat = [sing]notesBeat.png NotePlainLeft = [sing]notesPlainLeft.png NotePlainMid = [sing]notesPlainMid.png NotePlainRight = [sing]notesPlainRight.png @@ -225,6 +227,7 @@ NoteBGLeft = [sing]notesBgLeft.png NoteBGMid = [sing]notesBgMid.png NoteBGRight = [sing]notesBgRight.png Pause = [sing]pause.png +BeatClap = [sing]clap.png GrayLeftRap = [rap]notesLeft.png GrayMidRap = [rap]notesMid.png @@ -297,6 +300,7 @@ previousseq = [editsub]previous-seq.png undo = [editsub]undo.png gold = [editsub]gold.png freestyle = [editsub]freestyle.png +beatnote = [editsub]beat-note.png # # # COLOR PICKER # # # PickerBG = [color]pickerbg.png diff --git a/game/themes/Deluxe/Winter.ini b/game/themes/Deluxe/Winter.ini index 95fb29f4f..65eeee251 100644 --- a/game/themes/Deluxe/Winter.ini +++ b/game/themes/Deluxe/Winter.ini @@ -217,6 +217,8 @@ GrayLeft = [sing]notesLeft.png GrayMid = [sing]notesMid.png GrayRight = [sing]notesRight.png # unsung notes - colorized with playercolors +NoteBeatBG = [sing]notesBeatBG.png +NoteBeat = [sing]notesBeat.png NotePlainLeft = [sing]notesPlainLeft.png NotePlainMid = [sing]notesPlainMid.png NotePlainRight = [sing]notesPlainRight.png @@ -225,6 +227,7 @@ NoteBGLeft = [sing]notesBgLeft.png NoteBGMid = [sing]notesBgMid.png NoteBGRight = [sing]notesBgRight.png Pause = [sing]pause.png +BeatClap = [sing]clap.png GrayLeftRap = [rap]notesLeft.png GrayMidRap = [rap]notesMid.png @@ -297,6 +300,7 @@ previousseq = [editsub]previous-seq.png undo = [editsub]undo.png gold = [editsub]gold.png freestyle = [editsub]freestyle.png +beatnote = [editsub]beat-note.png # # # COLOR PICKER # # # PickerBG = [color]pickerbg.png diff --git a/game/themes/Deluxe/[editsub]beat-note.png b/game/themes/Deluxe/[editsub]beat-note.png new file mode 100644 index 0000000000000000000000000000000000000000..44d5a10d2d1765c0c0ee8e370948edc4265640ce GIT binary patch literal 1179 zcmV;M1Z4Y(P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11Sm;F zK~zYIy_H*RR7D)de>3Oo>7L!*TPQ8%-WCcXS_ueJd=P^{5Dftm6MZleBS=h2jEO#& z7)jK4i3x-UAAG@t@L-Hw;!6`iLKIph5{;kR>HMcsq1O5E_NTvfvfU`i;#KE_KPN4Wc z$)5r)1MdUQy(aAgjsx~TAa;SzfIgsg;-Hw&wt%ky1Dq}|5o-iup4OBH+LRUw!~q;f zcoB=VhJxqm8QRbbv;ijoeXk8FqYiBdv0H26OXfSgu%XzRU1w8O?NC|mP*v^FH(20< z14Hy&%yZ>>$d+}*s8AQ$&-(d9N@-)vzFdJ%fCRUtVs$Jkj%;j=#ddBji=viRm;lB4Zuq=b`)=F?bb5qg1RlyQ0o0`?zH?KyTj`)W34HUVb@ zP!n@S*XM6fD_h&*j)l|RAF%$ltIV#8@a`M6JhQ11D{6CTu)y{gyVoSn~C5_ko| zkpLZxXXez8K0EdfrLw+RD1oP+Tcj>H0k6~UV86qV@c1#|bU7z4(P3TcdCv~9q^h?f_`>6}QV zaw47b<)q6pN&YFu|FS`U5M1%5nmF$ddcY+K9P>vyR3`P)L}$HkByOA&!-?-h;J5{N z7RwSnI~h*GMVmYOO0&7?g&p5XXRRJDz1l t(o)K#Hm1+d4xa{2G5Y0BHm3g)e*;*i;uX?$)I|UQ002ovPDHLkV1h|@A>#l5 literal 0 HcmV?d00001 diff --git a/game/themes/Deluxe/[sing]clap.png b/game/themes/Deluxe/[sing]clap.png new file mode 100644 index 0000000000000000000000000000000000000000..4b9e9cdd24141f744949dabc5bb8a4215741ff76 GIT binary patch literal 412 zcmV;N0b~A&P)X1^@s6z#LUx00009a7bBm000fX z000fX0om*o1ONa48FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10VzpD zK~zYIy_QiDgdhlpu`@N}b6uzNbe(#3raQ3@g^LP^JI(%Y>mnZ@XaU|4R=>kbet!x}6=-~^5V9sydjmKUw;vT?LWz&+F< zLg?Lrk3-Q}52XExQnS=IIz2;PP|tvuwfD&W<*1mad$!nR8>aTL(l(OxBv%4MplAu5qSsH-{${ZC!$wh!xLk@Z9|Z5lrk7u4Gs(PCR-L8Kej%9 z3oiv;dS?f|*jxdC=VP!7WHOf+;6Y8LGyXI7x~`^iCKUQ`4q#MORAuykVjGWMxXKGH zhZj@;jAv!LfsCGWq3B^L#noj^-JPCx0E3aI`HE8}6;3d802o!WjwcsH6hWQ<7*XB1 zt3mwq0#p^i47NVo`h69Tj>7t@ge}LeBa1lfYK`RJ#aCI>X zqO&dzrQlZF1gA7EP4XPl))J|t@l0=c=erN?`-CXpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11Sm;F zK~zYIy_H*RR7D)de>3Oo>7L!*TPQ8%-WCcXS_ueJd=P^{5Dftm6MZleBS=h2jEO#& z7)jK4i3x-UAAG@t@L-Hw;!6`iLKIph5{;kR>HMcsq1O5E_NTvfvfU`i;#KE_KPN4Wc z$)5r)1MdUQy(aAgjsx~TAa;SzfIgsg;-Hw&wt%ky1Dq}|5o-iup4OBH+LRUw!~q;f zcoB=VhJxqm8QRbbv;ijoeXk8FqYiBdv0H26OXfSgu%XzRU1w8O?NC|mP*v^FH(20< z14Hy&%yZ>>$d+}*s8AQ$&-(d9N@-)vzFdJ%fCRUtVs$Jkj%;j=#ddBji=viRm;lB4Zuq=b`)=F?bb5qg1RlyQ0o0`?zH?KyTj`)W34HUVb@ zP!n@S*XM6fD_h&*j)l|RAF%$ltIV#8@a`M6JhQ11D{6CTu)y{gyVoSn~C5_ko| zkpLZxXXez8K0EdfrLw+RD1oP+Tcj>H0k6~UV86qV@c1#|bU7z4(P3TcdCv~9q^h?f_`>6}QV zaw47b<)q6pN&YFu|FS`U5M1%5nmF$ddcY+K9P>vyR3`P)L}$HkByOA&!-?-h;J5{N z7RwSnI~h*GMVmYOO0&7?g&p5XXRRJDz1l t(o)K#Hm1+d4xa{2G5Y0BHm3g)e*;*i;uX?$)I|UQ002ovPDHLkV1h|@A>#l5 literal 0 HcmV?d00001 diff --git a/game/themes/Modern/[sing]clap.png b/game/themes/Modern/[sing]clap.png new file mode 100644 index 0000000000000000000000000000000000000000..4b9e9cdd24141f744949dabc5bb8a4215741ff76 GIT binary patch literal 412 zcmV;N0b~A&P)X1^@s6z#LUx00009a7bBm000fX z000fX0om*o1ONa48FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10VzpD zK~zYIy_QiDgdhlpu`@N}b6uzNbe(#3raQ3@g^LP^JI(%Y>mnZ@XaU|4R=>kbet!x}6=-~^5V9sydjmKUw;vT?LWz&+F< zLg?Lrk3-Q}52XExQnS=IIz2;PP|tvuwfD&W<*1mad$!nR8>aTL(l(OxBv%4MplAu5qSsH-{${ZC!$wh!xLk@Z9|Z5lrk7u4Gs(PCR-L8Kej%9 z3oiv;dS?f|*jxdC=VP!7WHOf+;6Y8LGyXI7x~`^iCKUQ`4q#MORAuykVjGWMxXKGH zhZj@;jAv!LfsCGWq3B^L#noj^-JPCx0E3aI`HE8}6;3d802o!WjwcsH6hWQ<7*XB1 zt3mwq0#p^i47NVo`h69Tj>7t@ge}LeBa1lfYK`RJ#aCI>X zqO&dzrQlZF1gA7EP4XPl))J|t@l0=c=erN?`-CX Date: Sat, 2 Oct 2021 23:15:14 +0200 Subject: [PATCH 2/6] Core additions and algorithm --- src/base/UIni.pas | 199 +++- src/base/UMusic.pas | 1 + src/base/UNote.pas | 12 +- src/base/URecord.pas | 247 +++++ src/base/USong.pas | 16 + src/base/UXMLSong.pas | 1 + src/beatNote/UBeatNote.pas | 922 ++++++++++++++++++ src/beatNote/UBeatNoteTimer.pas | 323 ++++++ .../controllers/UScreenSingController.pas | 5 +- src/screens/views/UScreenSingView.pas | 4 + 10 files changed, 1726 insertions(+), 4 deletions(-) create mode 100644 src/beatNote/UBeatNote.pas create mode 100644 src/beatNote/UBeatNoteTimer.pas diff --git a/src/base/UIni.pas b/src/base/UIni.pas index d7b4ffede..30a8d0dcf 100644 --- a/src/base/UIni.pas +++ b/src/base/UIni.pas @@ -81,6 +81,30 @@ TInputDeviceConfig = record IPlayers: array[0..6] of UTF8String = ('1', '2', '3', '4', '6', '8', '12'); IPlayersVals: array[0..6] of integer = ( 1 , 2 , 3 , 4 , 6 , 8 , 12); + + // Specific beat detection settings, per channel + type + PChannelBeatDetectionSettings = ^TChannelBeatDetectionSettings; + TChannelBeatDetectionSettings = record + IntensityThreshold: integer; // Primary detection threshold, relative to full scale (in percent) + RiseRateFactor: integer; // Only from config file, not user interface (for simplicity). Rise rate relative to threshold. This is the index to values in IBeatDetectRiseRateFactorValues + MinPeakMillisecond: integer; // Only from config file, not user interface (for simplicity). Minimum duration of peak (above threshold) + DropAfterPeakPercent: integer; // Only from config file, not user interface (for simplicity). Fall after peak + TestTimeAfterPeak: integer; // Only from config file, not user interface (for simplicity). Delta time after peak to assess fall in intensity + end; + + PInputDeviceBeatDetectionConfig = ^TInputDeviceBeatDetectionConfig; + TInputDeviceBeatDetectionConfig = record + Name: string; //**< Name of the input device + Input: integer; //**< Index of the input source to use for recording + + // There are 1(mono), two(stereo) or sometimes many channels (software input like SoundFlower) per input + // source and so each one has its own beat detection settings + ChannelBeatDectectionSettings: array of TChannelBeatDetectionSettings; + + + end; + type //Options @@ -101,8 +125,9 @@ TIni = class procedure LoadInputDeviceCfg(IniFile: TMemIniFile); procedure SaveInputDeviceCfg(IniFile: TIniFile); + procedure SaveInputDeviceBeatDetectionCfg(IniFile: TIniFile); procedure LoadThemes(IniFile: TCustomIniFile); - + procedure LoadInputDeviceBeatDetectionCfg(IniFile: TMemIniFile); procedure LoadPaths(IniFile: TCustomIniFile); procedure LoadScreenModes(IniFile: TCustomIniFile); procedure LoadWebcamSettings(IniFile: TCustomIniFile); @@ -205,6 +230,15 @@ TIni = class SingTimebarMode: integer; JukeboxTimebarMode: integer; + // Beat detection + BeatPlayClapSignOn: integer; // Whether or not to show a little clap sign above the beat notes + BeatDetectionDelay: integer; // Delay in ms. This is typically less than the typical + // microphone delays (i.e. field MicDelay) because the sampling is more + // frequent and not the entire audio buffer is analyzed. A typical value + // is 20-40ms. + + InputDeviceBeatDetectionConfig: array of TInputDeviceBeatDetectionConfig; // Configuration of beat detection for different audio channels + // Controller Joypad: integer; Mouse: integer; @@ -437,6 +471,13 @@ TIni = class sSelectPlayer = 1; sOpenMenu = 2; + IBeatPlayClapSignOn: array[0..1] of UTF8String = ('Hide', 'Show'); + + IBeatDetectIntensityThreshold: array[0..10] of UTF8String = ('5%','10%','15%','20%','30%','40%','50%','60%','70%','80%','90%'); + IBeatDetectIntensityThresholdValues: array[0..10] of Integer = (5,10,15,20,30,40,50,60,70,80,90); + + IBeatDetectRiseRateFactorValues: array[0..5] of real = (1.5,2,2.5,3,4,5); + ILineBonus: array[0..1] of UTF8String = ('Off', 'On'); IPartyPopup: array[0..1] of UTF8String = ('Off', 'On'); @@ -477,6 +518,7 @@ TIni = class IDebugTranslated: array[0..1] of UTF8String = ('Off', 'On'); IAVDelay: array of UTF8String; IMicDelay: array of UTF8String; + IBeatDetectionDelay: array of UTF8String; IFullScreenTranslated: array[0..2] of UTF8String = ('Off', 'On', 'Borderless'); IVisualizerTranslated: array[0..3] of UTF8String = ('Off', 'WhenNoVideo', 'WhenNoVideoAndImage','On'); @@ -1186,6 +1228,151 @@ procedure TIni.LoadPaths(IniFile: TCustomIniFile); PathStrings.Free; end; +// Specifically load the beat detection configuration for the various input devices and channels +procedure TIni.LoadInputDeviceBeatDetectionCfg(IniFile: TMemIniFile); +var + DeviceCfgBeatDetection: PInputDeviceBeatDetectionConfig; + DeviceIndex: integer; + ChannelCount: integer; + ChannelIndex: integer; + BeatDetectionKeys: TStringList; + i: integer; +begin + BeatDetectionKeys := TStringList.Create(); + + // read all record-keys for filtering + IniFile.ReadSection('BeatDetection', BeatDetectionKeys); + + SetLength(InputDeviceBeatDetectionConfig, Length(InputDeviceConfig)); + + for DeviceIndex := 0 to Length(InputDeviceConfig)-1 do + begin + DeviceCfgBeatDetection:= @InputDeviceBeatDetectionConfig[DeviceIndex]; + ChannelCount := Length(InputDeviceConfig[Deviceindex].ChannelToPlayerMap); + SetLength(DeviceCfgBeatDetection.ChannelBeatDectectionSettings, ChannelCount); + + for ChannelIndex := 0 to High(DeviceCfgBeatDetection.ChannelBeatDectectionSettings) do + begin + DeviceCfgBeatDetection.ChannelBeatDectectionSettings[ChannelIndex].IntensityThreshold := + IniFile.ReadInteger('BeatDetection', Format('IntensityThresholdChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), 5); + + DeviceCfgBeatDetection.ChannelBeatDectectionSettings[ChannelIndex].RiseRateFactor := + IniFile.ReadInteger('BeatDetection', Format('RiseRateFactorChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), 2); + + DeviceCfgBeatDetection.ChannelBeatDectectionSettings[ChannelIndex].MinPeakMillisecond := + IniFile.ReadInteger('BeatDetection', Format('MinPeakMillisecondChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), 2); + + DeviceCfgBeatDetection.ChannelBeatDectectionSettings[ChannelIndex].DropAfterPeakPercent := + IniFile.ReadInteger('BeatDetection', Format('DropAfterPeakPercentChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), 40); + + DeviceCfgBeatDetection.ChannelBeatDectectionSettings[ChannelIndex].TestTimeAfterPeak := + IniFile.ReadInteger('BeatDetection', Format('TestTimeAfterPeakChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), 20); + + end; + end; + + BeatDetectionKeys.Free(); + +end; + + + +procedure TIni.SaveInputDeviceBeatDetectionCfg(IniFile: TIniFile); +var + DeviceIndex: integer; + ChannelIndex: integer; + IntensityThreshold: integer; + IntensityRiseRateFactor: integer; + IntensityMinPeak: integer; + IntensityDropAfterPeak: integer; + IntensityTestTimeAfterPeakChannel: integer; +begin + for DeviceIndex := 0 to High(InputDeviceConfig) do + begin + // DeviceName and DeviceInput + + + + for ChannelIndex := 0 to High(InputDeviceBeatDetectionConfig[DeviceIndex].ChannelBeatDectectionSettings) do + begin + IntensityThreshold := InputDeviceBeatDetectionConfig[DeviceIndex].ChannelBeatDectectionSettings[ChannelIndex].IntensityThreshold; + if IntensityThreshold >= 0 then + begin + IniFile.WriteInteger('BeatDetection', + Format('IntensityThresholdChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), + IntensityThreshold); + end + else + begin + IniFile.DeleteKey('BeatDetection', + Format('IntensityThresholdChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1])); + end; + + + IntensityRiseRateFactor := InputDeviceBeatDetectionConfig[DeviceIndex].ChannelBeatDectectionSettings[ChannelIndex].RiseRateFactor; + if IntensityRiseRateFactor >= 0 then + begin + IniFile.WriteInteger('BeatDetection', + Format('RiseRateFactorChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), + IntensityRiseRateFactor); + end + else + begin + IniFile.DeleteKey('BeatDetection', + Format('RiseRateFactorChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1])); + end; + + IntensityMinPeak := InputDeviceBeatDetectionConfig[DeviceIndex].ChannelBeatDectectionSettings[ChannelIndex].MinPeakMillisecond; + + if IntensityMinPeak >= 0 then + begin + IniFile.WriteInteger('BeatDetection', + Format('MinPeakMillisecondChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), + IntensityMinPeak); + end + else + begin + IniFile.DeleteKey('BeatDetection', + Format('MinPeakMillisecondChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1])); + end; + + + + IntensityDropAfterPeak := InputDeviceBeatDetectionConfig[DeviceIndex].ChannelBeatDectectionSettings[ChannelIndex].DropAfterPeakPercent; + if IntensityDropAfterPeak >= 0 then + begin + IniFile.WriteInteger('BeatDetection', + Format('DropAfterPeakPercentChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), + IntensityDropAfterPeak); + end + else + begin + IniFile.DeleteKey('BeatDetection', + Format('DropAfterPeakPercentChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1])); + end; + + + + IntensityTestTimeAfterPeakChannel := InputDeviceBeatDetectionConfig[DeviceIndex].ChannelBeatDectectionSettings[ChannelIndex].TestTimeAfterPeak; + if IntensityTestTimeAfterPeakChannel >= 0 then + begin + IniFile.WriteInteger('BeatDetection', + Format('TestTimeAfterPeakChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1]), + IntensityTestTimeAfterPeakChannel); + end + else + begin + IniFile.DeleteKey('BeatDetection', + Format('TestTimeAfterPeakChannel%d[%d]', [ChannelIndex+1, DeviceIndex+1])); + end; + + + + end; + end; + +end; + procedure TIni.LoadThemes(IniFile: TCustomIniFile); begin // No Theme Found @@ -1548,6 +1735,7 @@ procedure TIni.Load(); LoadThemes(IniFile); LoadInputDeviceCfg(IniFile); + LoadInputDeviceBeatDetectionCfg(IniFile); // LoadAnimation LoadAnimation := ReadArrayIndex(ILoadAnimation, IniFile, 'Advanced', 'LoadAnimation', IGNORE_INDEX, 'On'); @@ -1555,6 +1743,11 @@ procedure TIni.Load(); // ScreenFade ScreenFade := ReadArrayIndex(IScreenFade, IniFile, 'Advanced', 'ScreenFade', IGNORE_INDEX, 'On'); + // To show the clap sign or not + BeatPlayClapSignOn := ReadArrayIndex(IBeatPlayClapSignOn, IniFile, 'BeatPlay', 'BeatPlayClapSignOn', IGNORE_INDEX, 'Show'); + + BeatDetectionDelay := IniFile.ReadInteger('BeatPlay', 'BeatDetectionDelay', 140); + // Visualizations // this could be of use later.. // VisualizerOption := @@ -1864,6 +2057,7 @@ procedure TIni.Save; IniFile.WriteString('Themes', 'Color', IColor[Color]); SaveInputDeviceCfg(IniFile); + SaveInputDeviceBeatDetectionCfg(IniFile); //LoadAnimation IniFile.WriteString('Advanced', 'LoadAnimation', ILoadAnimation[LoadAnimation]); @@ -1895,6 +2089,9 @@ procedure TIni.Save; //SyncTo IniFile.WriteString('Advanced', 'SyncTo', ISyncTo[SyncTo]); + IniFile.WriteString('BeatPlay', 'BeatPlayClapSignOn', IBeatPlayClapSignOn[BeatPlayClapSignOn]); + IniFile.WriteInteger('BeatPlay', 'BeatDetectionDelay', BeatDetectionDelay); + // Joypad IniFile.WriteString('Controller', 'Joypad', IJoypad[Joypad]); diff --git a/src/base/UMusic.pas b/src/base/UMusic.pas index 4ddd7971d..e9d4a57e5 100644 --- a/src/base/UMusic.pas +++ b/src/base/UMusic.pas @@ -146,6 +146,7 @@ TLine = record * Normally just one set is defined but in duet mode it might for example * contain two sets. *) + PLines = ^TLines; TLines = record CurrentLine: integer; // for drawing of current line High: integer; // = High(Line)! diff --git a/src/base/UNote.pas b/src/base/UNote.pas index 7adb7269c..007f91536 100644 --- a/src/base/UNote.pas +++ b/src/base/UNote.pas @@ -47,7 +47,8 @@ interface UScreenSingController, UScreenJukebox, USong, - UTime; + UTime, + UBeatNote; type PPLayerNote = ^TPlayerNote; @@ -341,6 +342,12 @@ procedure Sing(Screen: TScreenSingController); // make some operations when detecting new voice pitch if (LyricsState.CurrentBeatD >= 0) and (LyricsState.OldBeatD <> LyricsState.CurrentBeatD) then NewBeatDetect(Screen); + + + // Beat detection is done at every input handling run (about every 40ms at a screen update frequency + // of 25Hz and not just on new beats as for the singing) + handleBeatNotes(Screen); + end; procedure SingJukebox(Screen: TScreenJukebox); @@ -524,7 +531,8 @@ procedure NewNote(CP: integer; Screen: TScreenSingController); // check if line is active if ((CurrentLineFragment.StartBeat <= ActualBeat) and (CurrentLineFragment.StartBeat + CurrentLineFragment.Duration-1 >= ActualBeat)) and - (CurrentLineFragment.NoteType <> ntFreestyle) and // but ignore FreeStyle notes + (CurrentLineFragment.NoteType <> ntFreestyle) and // but ignore FreeStyle notes + ((CurrentLineFragment.NoteType <> ntRap) or not CurrentSong.RapBeat) and // If beat mode is on, rap notes are handled separately (CurrentLineFragment.Duration > 0) then // and make sure the note length is at least 1 begin SentenceDetected := SentenceIndex; diff --git a/src/base/URecord.pas b/src/base/URecord.pas index 1750fdb40..1b3de825f 100644 --- a/src/base/URecord.pas +++ b/src/base/URecord.pas @@ -100,6 +100,12 @@ TCaptureBuffer = class // use to analyze sound from buffers to get new pitch procedure AnalyzeBuffer; + + // Beat detection: short burst of sound above background noise level, within a short time window back from the + // current time point (timeBack parametr, unit seconds, depends on the current time point) + procedure AnalyzeBufferBeatOnly(timeBack: real; Threshold: integer; RiseRate: integer; + MinPeakDuration: integer; DropAfterPeak: integer; TestTimeAfterPeak: integer); + procedure LockAnalysisBuffer(); {$IFDEF HasInline}inline;{$ENDIF} procedure UnlockAnalysisBuffer(); {$IFDEF HasInline}inline;{$ENDIF} @@ -400,6 +406,247 @@ procedure TCaptureBuffer.AnalyzeBuffer; end; end; + + +procedure TCaptureBuffer.AnalyzeBufferBeatOnly(timeBack: real; Threshold: integer; RiseRate: integer; MinPeakDuration: integer; + DropAfterPeak: integer; TestTimeAfterPeak: integer); +var + Volume: single; + MaxVolume: single; + MeanVolume: single; + SampleIndex, SampleInterval, SampleIndexPeak, StartSampleIndex: integer; + BaselineStart, BaselineInterval, BaselineSampleIndex: integer; // To compare the peak sample values to baseline + SampleLowerLimit, SampleUpperLimit: integer; + detected, maximumdetected: Boolean; + RMSVolume, RMSVolumeBaseline, riserate_evaluated: single; + // Four potential criteria for a succesful beat note detetion (this is to discriminate against background noise) + passesThreshold: Boolean; // 1) Passing an absolute sound intensity threshold (anyways mandatory) + passesRiseRate: Boolean; // 2) Sufficiently quick rise rate (depending on user configuration in Ini variable) + passesDuration: Boolean; // 3) Sufficient duration (not just single off measurement, depends on configuration in Ini variable) + passesDropAfterPeak: Boolean; // 4) Sufficient quick fall after peak (depending on user configuration in Ini variable) + +begin + + + + ToneValid := false; + ToneAbs := -1; + Tone := -1; + detected := false; + + + passesThreshold:=false; + passesRiseRate:=false; + passesDuration:=false; + passesDropAfterPeak:=false; + + if RiseRate = 0 then + passesRiseRate := true; // No rise rate requirement, so passes anyways + + if MinPeakDuration = 0 then + passesDuration := true; // No minimal duration, so test passed anyways + + if DropAfterPeak =0 then + passesDropAfterPeak:= true; + + LockAnalysisBuffer(); + try + + StartSampleIndex:=High(AnalysisBuffer)-Round(timeBack*AudioFormat.SampleRate); + + if(StartSampleIndex < 0) then + StartSampleIndex := 0; + + for SampleIndex := StartSampleIndex to High(AnalysisBuffer) do + begin + + passesThreshold:=false; + passesRiseRate:=false; + passesDuration:=false; + passesDropAfterPeak:=false; + + if RiseRate = 0 then + passesRiseRate := true; // No rise rate requirement, so passes anyways + + if MinPeakDuration = 0 then + passesDuration := true; // No minimal duration, so test passed anyways + + if DropAfterPeak =0 then + passesDropAfterPeak:= true; + + Volume := Abs(AnalysisBuffer[SampleIndex]) / (-Low(smallint)) *100; + if Volume > Threshold then + begin + passesThreshold:=true; + end; + + // Before going further, check whether by any chance we already pass all criteria + if passesThreshold and passesRiseRate and passesDuration and passesDropAfterPeak then + begin + detected:=true; + Break; + end; + + // First test passed, check for rise rate if necessary + if passesThreshold and (not passesRiseRate) then begin + BaselineStart:=SampleIndex-Round(0.005*AudioFormat.SampleRate); + if BaselineStart >= Low(AnalysisBuffer) then + begin + BaselineInterval:=Round(0.004*AudioFormat.SampleRate); + RMSVolumeBaseline:=0; + for BaselineSampleIndex := BaselineStart to BaselineStart+BaselineInterval do + begin + RMSVolumeBaseline := + RMSVolumeBaseline+(AnalysisBuffer[BaselineSampleIndex])*(AnalysisBuffer[BaselineSampleIndex])/(-Low(smallint))/(-Low(smallint)); + end; + RMSVolumeBaseline:=Sqrt(RMSVolumeBaseline/(BaselineInterval+1)); + BaselineStart:=SampleIndex; + BaselineInterval:=Round(0.003*AudioFormat.SampleRate); + if BaselineStart+BaselineInterval <= High(AnalysisBuffer) then + begin + + RMSVolume:=0; + for BaselineSampleIndex := BaselineStart to BaselineStart+BaselineInterval do + begin + RMSVolume := + RMSVolume+(AnalysisBuffer[BaselineSampleIndex])*(AnalysisBuffer[BaselineSampleIndex])/(-Low(smallint))/(-Low(smallint)); + end; + RMSVolume:=Sqrt(RMSVolume/(BaselineInterval+1)); + + + + riserate_evaluated:=(RMSVolume-RMSVolumeBaseline)*100; + // The idea is that we want to have quick rise but then also something that stays a bit constant or continues to rise + if (riserate_evaluated>=RiseRate) and (RMSVolume*100.0 > Volume/2.0) then + begin + passesRiseRate:=true; + + end; + + end; // End we can get the peak RMS + end; + + end; + + // Again, check whether by any chance we already pass all criteria + if passesThreshold and passesRiseRate and passesDuration and passesDropAfterPeak then + begin + detected:=true; + Break; + end; + + // First two tests OK, but not (yet) the third one + if passesThreshold and passesRiseRate and (not passesDuration) then + begin + SampleUpperLimit:=SampleIndex+Round(0.001*AudioFormat.SampleRate*MinPeakDuration*2); + if SampleUpperLimit > High(AnalysisBuffer) then + SampleUpperLimit := High(AnalysisBuffer); + SampleLowerLimit:=SampleIndex+Round(0.001*AudioFormat.SampleRate*MinPeakDuration); + if SampleLowerLimit > High(AnalysisBuffer) then + SampleLowerLimit := High(AnalysisBuffer); + maximumdetected:=false; + for BaselineSampleIndex := SampleLowerLimit to SampleUpperLimit do + begin + if Abs(AnalysisBuffer[BaselineSampleIndex]) / (-Low(smallint)) *100 > Threshold then + begin + maximumdetected:=true; + Break; + end; + end; + if maximumdetected then begin + passesDuration:=true; + end; + + end; + + // Again, check whether by any chance we already pass all criteria + if passesThreshold and passesRiseRate and passesDuration and passesDropAfterPeak then + begin + detected:=true; + Break; + end; + + // + if passesThreshold and passesRiseRate and passesDuration and (not passesDropAfterPeak) then + begin + + + BaselineStart:=SampleIndex; + BaselineInterval:=Round(0.003*AudioFormat.SampleRate); + if BaselineStart+BaselineInterval <= High(AnalysisBuffer) then + begin + + RMSVolumeBaseline:=0; + for BaselineSampleIndex := BaselineStart to BaselineStart+BaselineInterval do + begin + RMSVolumeBaseline := + RMSVolumeBaseline+(AnalysisBuffer[BaselineSampleIndex])*(AnalysisBuffer[BaselineSampleIndex])/(-Low(smallint))/(-Low(smallint)); + end; + RMSVolumeBaseline:=Sqrt(RMSVolumeBaseline/(BaselineInterval+1)); + SampleUpperLimit:=SampleIndex+Round((TestTimeAfterPeak/1000.0+0.005)*AudioFormat.SampleRate); + SampleLowerLimit:=SampleIndex+Round(TestTimeAfterPeak/1000.0*AudioFormat.SampleRate); + // Avoid indexing error by accessing non-existent points + if SampleUpperLimit <= High(AnalysisBuffer) then + begin + RMSVolume:=0; + for BaselineSampleIndex := SampleLowerLimit to SampleUpperLimit do + begin + RMSVolume:=RMSVolume+(AnalysisBuffer[BaselineSampleIndex])*(AnalysisBuffer[BaselineSampleIndex])/(-Low(smallint))/(-Low(smallint)); + end; + RMSVolume:=Sqrt(RMSVolume/(SampleUpperLimit-SampleLowerLimit+1)); + + + // Fall rate is relative to max peak intensity (here, RMSVolumeBaseline taken on 1ms peak from initial detection on) + if ((RMSVolumeBaseline - RMSVolume)/RMSVolumeBaseline)*100.0 >= DropAfterPeak then + begin + passesDropAfterPeak:=true; + end; + end; + + + + end; + + + + end; + + // Final test if everything passes + if passesThreshold and passesRiseRate and passesDuration and passesDropAfterPeak then + begin + detected:=true; + Break; + end; + + + + + end; + + + + + + + + if detected then + begin + ToneValid := true; + ToneAbs:=48; + Tone:=0; + + + + end; + + finally + UnlockAnalysisBuffer(); + end; + +end; + + + function TCaptureBuffer.ArrayIndexOfMinimum(const AValues: array of real): Integer; var LValIdx: Integer; diff --git a/src/base/USong.pas b/src/base/USong.pas index 812132b77..8a183b385 100644 --- a/src/base/USong.pas +++ b/src/base/USong.pas @@ -158,6 +158,7 @@ TSong = class Resolution: integer; BPM: array of TBPM; GAP: real; // in miliseconds + RapBeat: boolean; // rap notes: clapping (true) or standard (false) Encoding: TEncoding; PreviewStart: real; // in seconds @@ -975,6 +976,9 @@ function TSong.ReadXMLHeader(const aFileName : IPath): boolean; //Language Sorting self.Language := Parser.SongInfo.Header.Language; self.LanguageNoAccent := LowerCase(GetStringWithNoAccents(UTF8Decode(self.Language))); + + //Rap beat + self.RapBeat:=Parser.SongInfo.Header.RapBeat; end else Log.LogError('File incomplete or not SingStar XML (A): ' + aFileName.ToNative); @@ -1039,6 +1043,7 @@ function TSong.ReadTXTHeader(SongFile: TTextFileStream; ReadCustomTags: Boolean) end; end; begin + Result := true; Done := 0; MedleyFlags := 0; @@ -1188,6 +1193,17 @@ function TSong.ReadTXTHeader(SongFile: TTextFileStream; ReadCustomTags: Boolean) else Log.LogError('Can''t find video file in song: ' + FullFileName); end + // Rap beat mode for detection of clapping on rap notes + else if (Identifier = 'RAP') then + begin + if Value = 'BEAT' then + begin + self.RapBeat:= true; + + end + else self.RapBeat:=false; + + end // Video Gap else if (Identifier = 'VIDEOGAP') then diff --git a/src/base/UXMLSong.pas b/src/base/UXMLSong.pas index 9fb4d1912..c0cccba79 100644 --- a/src/base/UXMLSong.pas +++ b/src/base/UXMLSong.pas @@ -68,6 +68,7 @@ TSongInfo = record Genre: UTF8String; Year: UTF8String; Language: UTF8String; + RapBeat: Boolean; // For now, not parsed, false by definition end; CountSentences: Cardinal; Sentences: ASentence; diff --git a/src/beatNote/UBeatNote.pas b/src/beatNote/UBeatNote.pas new file mode 100644 index 000000000..9bf2b4a77 --- /dev/null +++ b/src/beatNote/UBeatNote.pas @@ -0,0 +1,922 @@ +{* 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 UBeatNote; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + UBeatNoteTimer, // Provides the detection of beat notes along the time + UCommon, + UScreenSingController, + UMusic, + USong, + SysUtils, + UIni, + URecord, + UGraphicClasses; // Provides ScreenSong + +type +TBeatDetectionParameters = record // single structure to hold the beat detection parameters + Threshold: integer; // Threshold, in percent relative to full range (-low(smallint)) + RiseRate: integer; // Rise rate at the beginning of the pulse, in percent full range per ms. Provide 0 to skip check + MinPeakDuration: integer; // Minimal peak duration before intensity falls definitely below Threshold. Provide 0 to skip check + DropAfterPeak: integer; // The relative drop that we expect after the initial peak + TestTimeAfterPeak: integer; // How many milliseconds after the peak we should check for the drop + end; + +procedure handleBeatNotes(Screen: TScreenSingController); // General handler called at every cycle. This is NewBeatDetect from +// UNote with adaptation + +procedure checkBeatNote(CP: integer; Screen: TScreenSingController); // Specifically: detect current beat note (if any). This is +// This is NewNote from UNote with adaptation + +function TimeTolerance(PlayerIndex: integer): real; // As a function of the difficulty, get the tolerance in time length + +function BeatTolerance(PlayerIndex: integer): real; // As a function of the difficulty, get the tolerance in musc beat length + +procedure SingDrawLineBeats(Left, Top, Right: real; Track, PlayerNumber: integer; LineSpacing: integer); + +procedure SingDrawPlayerBGLineBeats(Left, Top, Right: real; Track, PlayerIndex: integer; LineSpacing: integer); + +procedure SingDrawPlayerLineBeats(X, Y, W: real; Track, PlayerIndex: integer; LineSpacing: integer); + +function spansCurrentBeat(ActualBeat: real; lineFragmentStartBeat: integer; + lineFragmentNoteType: TNoteType; lineFragmentDuration: integer; + tolerance: real ): boolean; + +function hitsCurrentBeat(ActualBeat: real; lineFragmentStartBeat: integer; + lineFragmentNoteType: TNoteType; lineFragmentDuration: integer; + tolerance: real ): boolean; + +function beatDistanceToNote(ActualBeat: real; lineFragmentStartBeat: integer; + lineFragmentDuration: integer ): real; + +function beatInNote(ActualBeat: real; lineFragmentStartBeat: integer; + lineFragmentDuration: integer ): integer; // Returns the nearest beat within the note + +function BeatDetectionParametersFromIni (PlayerIndex: integer): TBeatDetectionParameters; +function BeatDetectionParametersForDeviceAndChannel(DeviceIndex: integer; ChannelIndex: integer): TBeatDetectionParameters; + + +function GetTimeFromBeatReal(Beat: real; SelfSong: TSong = nil): real; // Timing function for AudioPlayback, with non-integer beat values +// Basically a copy from UNote.pas (GetTimeFromBeat) + +function getActualBeatUsingBeatDetectionDelay():real; // For the beat detection, +// we make use of the "midBeatD" variable in LyricsState, as usually. +// The sampling is different though: while for sing notes, new notes are triggered +// when midBeatD changes integral (floor) value, see the definition of CurrentBeatD +// in TLyricsState.UpdateBeats in UBeatTimer.pas. Here, we still need this information, +// but whether a given time-points falls within the beat or beat silence note with the +// desired precision is determined separately, by the procedure spansCurrentBeat. +// Also, the sampling is different. For regular sing note sampling, generally 4096 samples +// are used, which at a typical 44.1kHz spans some 100ms. This is a bit long for precise beat detection, so +// we cut down the sample here to the part that actually falls in the correct interval. Together with +// roughly 25Hz screen renewal and thus audio sampling, that makes some 40ms delay - not 100+40ms as for +// typical microphone sampling. Hence, we use here the delay specifically set for beat detection, not the +// genreal microphone delay as elsewhere. + + + + +implementation + +uses + UNote, + Math, + UGraphic, + dglOpenGL, + UDraw, + ULyrics; // Drawing routines; + +function getActualBeatUsingBeatDetectionDelay():real; + +begin + // Directly look for the beat where we are, using the beat detection delay set by the user, + // instead of the microphone delay set for singing. + Result:=LyricsState.MidBeatD - GetMidBeat((Ini.BeatDetectionDelay-Ini.MicDelay)/1000.0);; + + + +end; + +procedure handleBeatNotes(Screen: TScreenSingController); +var + + SentenceMin: integer; + SentenceMax: integer; + MaxCP, PlayerIndex: integer; + CP: integer; + J: cardinal; +begin + + + + MaxCP := 0; + if (CurrentSong.isDuet) and (PlayersPlay <> 1) then + MaxCP := 1; + + if (assigned(Screen)) then + begin + BeatNoteTimerState.analyzeBeatNoteTiming(Screen); + for J := 0 to MaxCP do + begin + CP := J; + + for PlayerIndex := 0 to PlayersPlay-1 do + begin + if (not CurrentSong.isDuet) or (PlayerIndex mod 2 = CP) then + begin + checkBeatNote(CP, Screen); + end + end + end; + + end; + + end; + +function TimeTolerance(PlayerIndex: integer): real; +var + tolerance: real; + beatTimeS: real; +begin + tolerance := 0.1; + + if (ScreenSong.Mode = smNormal) then + begin + case Ini.PlayerLevel[PlayerIndex] of + 0: tolerance:=0.15; + 1: tolerance:=0.1; + 2: tolerance:=0.06; + end; + + end + else + case Ini.Difficulty of + 0: tolerance:=0.15; + 1: tolerance:=0.1; + 2: tolerance:=0.06; + end; + + + + Result:=tolerance; + +end; + +function BeatTolerance(PlayerIndex: integer): real; +begin + Result:=TimeTolerance(PlayerIndex)/60.0*currentSong.BPM[0].BPM; +end; + +function BeatDetectionParametersFromIni(PlayerIndex: integer): TBeatDetectionParameters; +var + DeviceIndex: integer; + DeviceIndexToUse: integer; + Device: TAudioInputDevice; + DeviceCfg: PInputDeviceConfig; + ChannelIndex: integer; + ChannelIndexToUse: integer; + thePlayer: integer; +begin + DeviceIndexToUse:=-1; + // Look for the device used by the player + for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do + begin + Device := AudioInputProcessor.DeviceList[DeviceIndex]; + if not assigned(Device) then + continue; + DeviceCfg := @Ini.InputDeviceConfig[Device.CfgIndex]; + + // check if device is used + for ChannelIndex := 0 to High(DeviceCfg.ChannelToPlayerMap) do + begin + thePlayer := DeviceCfg.ChannelToPlayerMap[ChannelIndex] - 1; + if thePlayer = PlayerIndex then + begin + DeviceIndexToUse:=DeviceIndex; + ChannelIndexToUse:=ChannelIndex; + break; + end; + + end; + + end; + + + + Result:=BeatDetectionParametersForDeviceAndChannel(DeviceIndexToUse,ChannelIndexToUse); + + +end; + + +function BeatDetectionParametersForDeviceAndChannel(DeviceIndex: integer; ChannelIndex: integer): TBeatDetectionParameters; +var + returnVal: TBeatDetectionParameters; + getIndex: integer; +begin + returnVal.Threshold:=50; + returnVal.RiseRate:=20; + returnVal.MinPeakDuration:=2; + returnVal.DropAfterPeak:=40; + returnVal.TestTimeAfterPeak:=20; + if DeviceIndex < 0 then // No device found, return default values + begin + // nothing to do specifically + end + else + begin // This is a bit complicated since the ini settings contain the index to the correct value + // rather than the values themselves. So we need to get them from indexing of the IBeatDetect... arrays. + + if DeviceIndex > High(Ini.InputDeviceBeatDetectionConfig) then + DeviceIndex := High(Ini.InputDeviceBeatDetectionConfig); + + // No channels configured yet in the ini file, use default values + if Length(Ini.InputDeviceBeatDetectionConfig[DeviceIndex]. + ChannelBeatDectectionSettings)= 0 then begin + + // nothing to specifically + + end + else + begin + + if ChannelIndex<0 then + ChannelIndex := 0; + if ChannelIndex > High(Ini.InputDeviceBeatDetectionConfig[DeviceIndex]. + ChannelBeatDectectionSettings) then + ChannelIndex:=High(Ini.InputDeviceBeatDetectionConfig[DeviceIndex]. + ChannelBeatDectectionSettings); + + + getIndex:=Ini.InputDeviceBeatDetectionConfig[DeviceIndex]. + ChannelBeatDectectionSettings[ChannelIndex].IntensityThreshold; + if getIndex <0 then getIndex:=0; + if getIndex >High(IBeatDetectIntensityThresholdValues) then getIndex:= High(IBeatDetectIntensityThresholdValues); + + returnVal.Threshold:=IBeatDetectIntensityThresholdValues[getIndex]; + + // next, the riserate factor + getIndex:=Ini.InputDeviceBeatDetectionConfig[DeviceIndex]. + ChannelBeatDectectionSettings[ChannelIndex].RiseRateFactor; + if getIndex <0 then getIndex:=0; + if getIndex >High(IBeatDetectRiseRateFactorValues) then getIndex:= High(IBeatDetectRiseRateFactorValues); + + returnVal.RiseRate:=Round(returnVal.Threshold/IBeatDetectRiseRateFactorValues[getIndex]); + // This is done relatively to threshold to allow for main configuration through the threshold value + + // The other values are integers read directly from the ini system (for now without GUI access) + returnVal.MinPeakDuration:=Ini.InputDeviceBeatDetectionConfig[DeviceIndex]. + ChannelBeatDectectionSettings[ChannelIndex].MinPeakMillisecond; + returnVal.DropAfterPeak:=Ini.InputDeviceBeatDetectionConfig[DeviceIndex]. + ChannelBeatDectectionSettings[ChannelIndex].DropAfterPeakPercent; + returnVal.TestTimeAfterPeak:=Ini.InputDeviceBeatDetectionConfig[DeviceIndex]. + ChannelBeatDectectionSettings[ChannelIndex].TestTimeAfterPeak; + + + + + + + end; + + end; + + Result := returnVal; + +end; + +function beatInNote(ActualBeat: real; lineFragmentStartBeat: integer; + lineFragmentDuration: integer ): integer; +begin + Result := lineFragmentStartBeat; + if (ActualBeat > lineFragmentStartBeat) and (ActualBeat < lineFragmentStartBeat+lineFragmentDuration-1) then + Result := Round(ActualBeat); + if (ActualBeat >= lineFragmentStartBeat+lineFragmentDuration-1) then + Result := lineFragmentStartBeat+lineFragmentDuration-1; + +end; + + + + function beatDistanceToNote(ActualBeat: real; lineFragmentStartBeat: integer; + lineFragmentDuration: integer ): real; + begin + Result:=0; + if ActualBeat < lineFragmentStartBeat then Result := Abs( ActualBeat-lineFragmentStartBeat); + if ActualBeat > lineFragmentStartBeat + max(lineFragmentDuration-1,0) then + Result := Abs(ActualBeat-lineFragmentStartBeat + max(lineFragmentDuration-1,0)); + end; + + +// The idea here is to see whether the note spans the current beat (real number) +// This can be because of the tolerance around the beginning of the note +// Or because of the actual note extension. Since this is beat notes, we take +// the note with a shift of -0.5 beats to be centered on the beats rather than the +// intervals +function spansCurrentBeat(ActualBeat: real; lineFragmentStartBeat: integer; + lineFragmentNoteType: TNoteType; lineFragmentDuration: integer; + tolerance: real ): boolean; +var toleranceBreak:real; +begin +if (lineFragmentNoteType = ntRap) and CurrentSong.RapBeat then +Result:= (ActualBeat >= lineFragmentStartBeat-max(tolerance/2.0,0.5)) and + (ActualBeat < lineFragmentStartBeat + max(tolerance/2.0,lineFragmentDuration-0.5)) + +else + Result := false; + +end; + +// The beat note is hit if the actual beat falls in the tolerance interval +// around its start; this is for now intependent of the duration +function hitsCurrentBeat(ActualBeat: real; lineFragmentStartBeat: integer; + lineFragmentNoteType: TNoteType; lineFragmentDuration: integer; + tolerance: real ): boolean; +var toleranceBreak:real; +begin +if (lineFragmentNoteType = ntRap) and CurrentSong.RapBeat then +Result:= (ActualBeat >= lineFragmentStartBeat-tolerance/2.0) and + (ActualBeat < lineFragmentStartBeat + tolerance/2.0) +else + Result := false; + +end; + + +procedure checkBeatNote(CP: integer; Screen: TScreenSingController); // Specifically: detect current beat note (if any). This is +// inspired by NewNote from UNote with adaptation to the beat detection system (i.e. UBeateNoteTimer and specifically BeatNoteTimerState) +// The idea is that BeatNoteTimerState provides the information on whether there is a beat note (NoteType ntRap) is playing at present. +// If so, sound analysis is carried out (via the specific procedure AnalyzeBufferBeatOnly +// implemented in URecord. If a clap (loud sound compared to background and at absolute level) is detected by AnalyzeBufferBeatOnly +// in the expected time-frame (centered on the initiating beat), then the beat +// is considered to be hit. If a later part of the note is hit, it is considered missed +var + CurrentLineFragment: PLineFragment; + CurrentSound: TCaptureBuffer; + CurrentPlayer: PPlayer; + ActualBeat: real; + validNoteFound: Boolean; + MaxSongPoints: integer; + CurNotePoints: real; + TimeElapsed: real; // This is the time the present note has already been running + BeatDetectionParams: TBeatDetectionParameters; + + +begin + ActualBeat := getActualBeatUsingBeatDetectionDelay(); + if BeatNoteTimerState.doBeatNoteDetection(CP,ActualBeat) then + begin + + + + // Get the actual beat, with beat detection delay + + + CurrentLineFragment := BeatNoteTimerState.playerBeatNoteState[CP].LineFragment; + + CurrentPlayer := @Player[CP]; + CurrentSound := AudioInputProcessor.Sound[CP]; + + + validNoteFound:=false; + + TimeElapsed := (ActualBeat-BeatNoteTimerState.playerBeatNoteState[CP].CurrentBeat)/currentSong.BPM[0].BPM*60.0; + + + BeatDetectionParams:=BeatDetectionParametersFromIni(CP); + // Add additional time for gathering the necessary samples + TimeElapsed := TimeElapsed + BeatDetectionParams.MinPeakDuration/1000.0+0.005; // Time for baseline before + if BeatDetectionParams.DropAfterPeak > 0 then + begin + TimeElapsed := TimeElapsed + BeatDetectionParams.TestTimeAfterPeak/1000.0; + end; + + + + if BeatNoteTimerState.playerBeatNoteState[CP].CurrentBeat = CurrentLineFragment.StartBeat then + begin + if CurrentLineFragment.notetype = ntRap then + begin + TimeElapsed := TimeElapsed+TimeTolerance(CP)/2.0; + + end; + + end + else + begin + TimeElapsed := TimeElapsed+0.5*60.0/currentSong.BPM[0].BPM; + end; + + + + + + + CurrentSound.AnalyzeBufferBeatOnly(TimeElapsed, + BeatDetectionParams.Threshold, + BeatDetectionParams.RiseRate, + BeatDetectionParams.MinPeakDuration, + BeatDetectionParams.DropAfterPeak, + BeatDetectionParams.TestTimeAfterPeak); + + validNoteFound:=CurrentSound.ToneValid; + + + + if validNoteFound then // In the beat analysis, we only take into account new notes, multiple detection of the same note is irrelevant + begin + Inc(CurrentPlayer.LengthNote); + Inc(CurrentPlayer.HighNote); + + SetLength(CurrentPlayer.Note, CurrentPlayer.LengthNote); + CurrentPlayer.Note[CurrentPlayer.HighNote].Start:=floor(ActualBeat); + CurrentPlayer.Note[CurrentPlayer.HighNote].Duration:=1; + CurrentPlayer.Note[CurrentPlayer.HighNote].Detect:=ActualBeat; + CurrentPlayer.Note[CurrentPlayer.HighNote].Tone := CurrentLineFragment.Tone; + CurrentPlayer.Note[CurrentPlayer.HighNote].Hit := false; + CurrentPlayer.Note[CurrentPlayer.HighNote].NoteType := CurrentLineFragment.NoteType; + CurrentPlayer.Note[CurrentPlayer.HighNote].Perfect:=false; + + if (Ini.LineBonus > 0) then + MaxSongPoints := MAX_SONG_SCORE - MAX_SONG_LINE_BONUS + else + MaxSongPoints := MAX_SONG_SCORE; + + // Note: ScoreValue is the sum of all note values of the song + // (MaxSongPoints / ScoreValue) is the points that a player + // gets for a hit of one beat of a normal note + // CurNotePoints is the amount of points that is meassured + // for a hit of the note per full beat + CurNotePoints := (MaxSongPoints / Tracks[CP].ScoreValue) * ScoreFactor[CurrentLineFragment.NoteType]; + + // We give points if hit on the first internal beat + if BeatNoteTimerState.BeatNoteHitAtPresent(CP, ActualBeat) then + begin + case CurrentLineFragment.NoteType of + ntRap: begin + // Since only the first beat can be hit, we need to count this to the full number of beats in the note + CurrentPlayer.Score := CurrentPlayer.Score + CurNotePoints*BeatNoteTimerState.playerBeatNoteState[CP].LineFragment.Duration; + CurrentPlayer.Note[CurrentPlayer.HighNote].Perfect:=true; + CurrentPlayer.Note[CurrentPlayer.HighNote].Hit := true; + GoldenRec.IncrementBeatLevel(); + end; + + end + + end + else + begin + case CurrentLineFragment.NoteType of + ntRap: CurrentPlayer.Score := CurrentPlayer.Score - CurNotePoints; + + end; + if CurrentLineFragment.NoteType =ntRap then + GoldenRec.ResetBeatLevel(); + + end; + + + + + + // TO DO BEAT: To indicate better smileys along + + + + + + // a problem if we use floor instead of round is that a score of + // 10000 points is only possible if the last digit of the total points + // for golden and normal notes is 0. + // if we use round, the max score is 10000 for most songs + // but a score of 10010 is possible if the last digit of the total + // points for golden and normal notes is 5 + // the best solution is to use round for one of these scores + // and round the other score in the opposite direction + // so we assure that the highest possible score is 10000 in every case. + CurrentPlayer.ScoreInt := round(CurrentPlayer.Score / 10) * 10; + + if (CurrentPlayer.ScoreInt < CurrentPlayer.Score) then + //normal score is floored so we have to ceil golden notes score + CurrentPlayer.ScoreGoldenInt := ceil(CurrentPlayer.ScoreGolden / 10) * 10 + else + //normal score is ceiled so we have to floor golden notes score + CurrentPlayer.ScoreGoldenInt := floor(CurrentPlayer.ScoreGolden / 10) * 10; + + + CurrentPlayer.ScoreTotalInt := CurrentPlayer.ScoreInt + + CurrentPlayer.ScoreGoldenInt + + CurrentPlayer.ScoreLineInt; + + + SingDraw; // Update the screen for the new note + BeatNoteTimerState.notifyNoteHit(CP,ActualBeat); + + end; // We have to add a new note + + + + end; // Beat note detection necessary + + +end; + + + + + + + +// Basically SingDrawLine from UDraw, but specifically for the beat notes +procedure SingDrawLineBeats(Left, Top, Right: real; Track, PlayerNumber: integer; LineSpacing: integer); +var + Rec: TRecR; + Count: integer; + TempR: real; + W, H: real; + GoldenStarPos: real; +begin +// We actually don't have a playernumber in this procedure, it should reside in Track - but it is always set to zero +// So we exploit this behavior a bit - we give Track the playernumber, keep it in playernumber - and then we set Track to zero +// This could also come quite in handy when we do the duet mode, cause just the notes for the player that has to sing should be drawn then +// BUT this is not implemented yet, all notes are drawn! :D + if (ScreenSing.settings.NotesVisible and (1 shl Track) <> 0) then + begin + //PlayerNumber := Track + 1; // Player 1 is 0 + + // exploit done + + + + + glColor3f(1, 1, 1); + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + if not Tracks[Track].Lines[Tracks[Track].CurrentLine].HasLength(TempR) then TempR := 0 + else TempR := (Right-Left) / TempR; + + + + with Tracks[Track].Lines[Tracks[Track].CurrentLine] do + begin + for Count := 0 to HighNote do + begin + with Notes[Count] do + begin + + if (NoteType = ntRap) and CurrentSong.RapBeat then + begin + glColor4f(1, 1, 1, 1); + + W := NotesW[PlayerNumber - 1] * 2 + 1; + H := NotesH[PlayerNumber - 1] * 1.5 + 3.5; + + + + // Technically, center on the beat (which is shifted by 0.5 compared to the standard notes + Rec.Right := (StartBeat - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat) * TempR + Left + 0.5 + 10*ScreenX + 2; + Rec.Left := Rec.Right - W; + Rec.Top := Top - (Tone-BaseNote)*LineSpacing/2 - W/2.0; + Rec.Bottom := Rec.Top + W; + + + + //ConsoleWriteLn(FloatToStr(Tex_Note_Beat[PlayerIndex+1].W)); + + //Syntax for test Tex_BG_Left[PlayerIndex+1].TexNum + glBindTexture(GL_TEXTURE_2D, Tex_Note_Beat[PlayerNumber].TexNum); + + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(Rec.Left, Rec.Top); + glTexCoord2f(0, 1); glVertex2f(Rec.Left, Rec.Bottom); + glTexCoord2f(1, 1); glVertex2f(Rec.Right, Rec.Bottom); + glTexCoord2f(1, 0); glVertex2f(Rec.Right, Rec.Top); + glEnd; + + // Optional clapping hands to indicate the nature of the beat notes + if Ini.BeatPlayClapSignOn=1 then + begin + glBindTexture(GL_TEXTURE_2D, Tex_Note_Clap.TexNum); + + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f((Rec.Left+Rec.Right)/2-8, Rec.Top-25); + glTexCoord2f(0, 1); glVertex2f((Rec.Left+Rec.Right)/2-8, Rec.Top-8); + glTexCoord2f(1, 1); glVertex2f((Rec.Left+Rec.Right)/2+11, Rec.Top-8); + glTexCoord2f(1, 0); glVertex2f((Rec.Left+Rec.Right)/2+11, Rec.Top-25); + glEnd; + + end; + + + end; // Note type is a beat + + end; // with + end; // for + end; // with + + glDisable(GL_BLEND); + glDisable(GL_TEXTURE_2D); + +end; + +end; + + +//draw Note glow +procedure SingDrawPlayerBGLineBeats(Left, Top, Right: real; Track, PlayerIndex: integer; LineSpacing: integer); +var + Rec: TRecR; + Count: integer; + TempR: real; + W, H: real; +begin + if (ScreenSing.settings.NotesVisible and (1 shl PlayerIndex) <> 0) then + begin + //glColor4f(1, 1, 1, sqrt((1+sin( AudioPlayback.Position * 3))/4)/ 2 + 0.5 ); + glColor4f(1, 1, 1, sqrt((1 + sin(AudioPlayback.Position * 3)))/2 + 0.05); + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + if not Tracks[Track].Lines[Tracks[Track].CurrentLine].HasLength(TempR) then TempR := 0 + else TempR := (Right-Left) / TempR; + + with Tracks[Track].Lines[Tracks[Track].CurrentLine] do + begin + for Count := 0 to HighNote do + begin + with Notes[Count] do + begin + if (NoteType = ntRap) and CurrentSong.RapBeat then + begin + W := NotesW[PlayerIndex] * 3 + 1.5; + H := NotesH[PlayerIndex] * 1.5 + 3.5; + + + + // The timing of the beats is differnt, centered on rhythmic beat, not mid interval + Rec.Right := (StartBeat - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat) * TempR + Left + 0.5 + 10*ScreenX + 4; + Rec.Left := Rec.Right - W; + Rec.Top := Top - (Tone-BaseNote)*LineSpacing/2 - W/2.0; + Rec.Bottom := Rec.Top + W; + + //ConsoleWriteLn(FloatToStr(Tex_Note_Beat[PlayerIndex+1].W)); + + //Syntax for test Tex_BG_Left[PlayerIndex+1].TexNum + glBindTexture(GL_TEXTURE_2D, Tex_Note_Beat_BG[PlayerIndex+1].TexNum); + + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(Rec.Left, Rec.Top); + glTexCoord2f(0, 1); glVertex2f(Rec.Left, Rec.Bottom); + glTexCoord2f(1, 1); glVertex2f(Rec.Right, Rec.Bottom); + glTexCoord2f(1, 0); glVertex2f(Rec.Right, Rec.Top); + glEnd; + + + + + + end + + end; // with + end; // for + end; // with + + glDisable(GL_BLEND); + glDisable(GL_TEXTURE_2D); + end; +end; + +// draw sung notes +procedure SingDrawPlayerLineBeats(X, Y, W: real; Track, PlayerIndex: integer; LineSpacing: integer); +var + TempR: real; + Rec: TRecR; + N: integer; +// R, G, B, A: real; + NotesH2: real; +begin + if (ScreenSing.Settings.InputVisible) then + begin + //Log.LogStatus('Player notes', 'SingDraw'); + + glColor3f(1, 1, 1); + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + //if Player[NrGracza].LengthNote > 0 then + begin + if not Tracks[Track].Lines[Tracks[Track].CurrentLine].HasLength(TempR) then TempR := 0 + else TempR := W / TempR; + + for N := 0 to Player[PlayerIndex].HighNote do + begin + with Player[PlayerIndex].Note[N] do + begin + if (NoteType = ntRap) and CurrentSong.RapBeat then + begin + + + // Left part of note + + Rec.Right := X+(Detect - Duration/4.0- Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat) * TempR + 10*ScreenX; + Rec.Left := Rec.Right - NotesW[PlayerIndex]; + + //Rec.Left := X + (Start - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat) * TempR + 0.5 + 10*ScreenX; + //Rec.Right := Rec.Left + NotesW[PlayerIndex]; + + // Draw it in half size, if not hit + if Hit then + begin + NotesH2 := NotesH[PlayerIndex] + end + else + begin + NotesH2 := int(NotesH[PlayerIndex] * 0.65); + end; + + + Rec.Top := Y - (Tone-Tracks[Track].Lines[Tracks[Track].CurrentLine].BaseNote)*LineSpacing/2 - NotesH2; + if not Hit then + Rec.Top := Y-NotesH2; + + Rec.Bottom := Rec.Top + 2 * NotesH2; + + // draw the left part + glColor3f(1, 1, 1); + + glBindTexture(GL_TEXTURE_2D, Tex_Left[PlayerIndex+1].TexNum); + + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(Rec.Left, Rec.Top); + glTexCoord2f(0, 1); glVertex2f(Rec.Left, Rec.Bottom); + glTexCoord2f(1, 1); glVertex2f(Rec.Right, Rec.Bottom); + glTexCoord2f(1, 0); glVertex2f(Rec.Right, Rec.Top); + glEnd; + + // Middle part of the note + Rec.Left := Rec.Right; + Rec.Right := X + (Detect + Duration/4.0 - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat) * + TempR - NotesW[PlayerIndex] + 10*ScreenX; + + // new + //if (Start + Duration - 1 = LyricsState.CurrentBeatD) then + // Rec.Right := Rec.Right - (1-Frac(LyricsState.MidBeatD)) * TempR; + + // the left note is more right than the right note itself, sounds weird - so we fix that xD + if Rec.Right <= Rec.Left then + Rec.Right := Rec.Left; + + // draw the middle part + + glBindTexture(GL_TEXTURE_2D, Tex_Mid[PlayerIndex+1].TexNum); + glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); + glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT ); + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(Rec.Left, Rec.Top); + glTexCoord2f(0, 1); glVertex2f(Rec.Left, Rec.Bottom); + glTexCoord2f(round((Rec.Right-Rec.Left)/32), 1); glVertex2f(Rec.Right, Rec.Bottom); + glTexCoord2f(round((Rec.Right-Rec.Left)/32), 0); glVertex2f(Rec.Right, Rec.Top); + glEnd; + glColor3f(1, 1, 1); + + // the right part of the note + Rec.Left := Rec.Right; + Rec.Right := Rec.Right + NotesW[PlayerIndex]; + + glBindTexture(GL_TEXTURE_2D, Tex_Right[PlayerIndex+1].TexNum); + + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(Rec.Left, Rec.Top); + glTexCoord2f(0, 1); glVertex2f(Rec.Left, Rec.Bottom); + glTexCoord2f(1, 1); glVertex2f(Rec.Right, Rec.Bottom); + glTexCoord2f(1, 0); glVertex2f(Rec.Right, Rec.Top); + glEnd; + + // Perfect note is stored + if Hit and (Ini.EffectSing=1) then + begin + //A := 1 - 2*(LyricsState.GetCurrentTime() - GetTimeFromBeat(Start + Duration)); + //if not (Start + Duration - 1 = LyricsState.CurrentBeatD) then + begin + //Star animation counter + //inc(Starfr); + //Starfr := Starfr mod 128; + // if not(CurrentSong.isDuet) or (PlayerIndex mod 2 = Track) then + + if (NoteType = ntRap) then + GoldenRec.SavePerfectBeatPos((Rec.Left+Rec.Right)/2.0, (Rec.Top+Rec.Bottom)/2.0); + + + + + end; + end; + end; // it's a beat note + end; // with + end; // for + + + end; // if + end; // if +end; + +function GetTimeFromBeatReal(Beat: real; SelfSong: TSong = nil): real; +var + CurBPM: integer; + Song: TSong; +begin + + if (SelfSong <> nil) then + Song := SelfSong + else + Song := CurrentSong; + + Result := 0; + + // static BPM + if Length(Song.BPM) = 1 then + begin + Result := Song.GAP / 1000 + Beat * 60 / Song.BPM[0].BPM; + end + // variable BPM + else if Length(Song.BPM) > 1 then + begin + Result := Song.GAP / 1000; + CurBPM := 0; + while (CurBPM <= High(Song.BPM)) and + (Beat > Song.BPM[CurBPM].StartBeat) do + begin + if (CurBPM < High(Song.BPM)) and + (Beat >= Song.BPM[CurBPM+1].StartBeat) then + begin + // full range + Result := Result + (60 / Song.BPM[CurBPM].BPM) * + (Song.BPM[CurBPM+1].StartBeat - Song.BPM[CurBPM].StartBeat); + end; + + if (CurBPM = High(Song.BPM)) or + (Beat < Song.BPM[CurBPM+1].StartBeat) then + begin + // in the middle + Result := Result + (60 / Song.BPM[CurBPM].BPM) * + (Beat - Song.BPM[CurBPM].StartBeat); + end; + Inc(CurBPM); + end; + + { + while (Time > 0) do + begin + GetMidBeatSub(CurBPM, Time, CurBeat); + Inc(CurBPM); + end; + } + end + // invalid BPM + else + begin + Result := 0; + end; +end; + + + + + +end. + diff --git a/src/beatNote/UBeatNoteTimer.pas b/src/beatNote/UBeatNoteTimer.pas new file mode 100644 index 000000000..3be2fa115 --- /dev/null +++ b/src/beatNote/UBeatNoteTimer.pas @@ -0,0 +1,323 @@ +{* 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 UBeatNoteTimer; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + UMusic, // Generically, the song structure, notes + UIni, // For the number of players // + UCommon, + UScreenSingController; + +type + // Elementary storage, for a given player, of the currently playing + // beat or beat silence note (or nil for the LineFragment field + // if none of these notes is playing) + TPlayerBeatNoteState = record + CurrentBeat: integer; // current beat (rounded) + LineFragment: PLineFragment; // Points to current line fragment (note) for each player or nil if none available + NoteHit: boolean; // Records whether at all the present note has been hit + NoteHitExactBeatTime: real; // For drawing, exact timing of last hit + NoteLastHitOnBeat: integer; // Records the last hit of this note (there can be as many hits as there are beats in the note) + end; + + + + + type + // This class acts to indicate the state of the advancing song in terms of beat + // notes / silences playing at a given time. For each player + // this information is stored as a record of type TPlayerBeatNoteState + TBeatNoteTimerState = class // This class handles indication of beat/beat silence notes playing (or not) + public + playerBeatNoteState: array of TPlayerBeatNoteState; // Array indicating where each player is at present + playerLastHitBeatTime: array of real; // Array keeping track of players last hit in terms of lyrics time + constructor Create; + procedure analyzeBeatNoteTiming(Screen: TScreenSingController); // Analyze present timing for all player to see whether ther are currently + // Beat note or beat breaks playing + procedure analyzeBeatNoteTimingForPlayer(CP: integer); // Analysis for a given player + + function doBeatNoteDetection(CP: integer; ActualBeat: real):Boolean; // Function that indicates whether for a given player, beat note/break detection should be done + // at present. This is the case if the current beat note/break has not yet been hit on the current beat (breaks can have a longer duration) + + function BeatNoteHitAtPresent(CP: integer; ActualBeat: real):Boolean; // Indicates whether the currently playing beat note (if any) is hit on its + // first beat. This function needs to invoked after doBeatNoteDetection, in the same cycle (i.e. the ActualBeat timing value being the same) + + procedure notifyNoteHit(CP: integer; ActualBeat: real); // The current player has hit the current note, update the lcoal variables correspondingly + end; + +procedure createTBeatNoteTimerState(); + +// global singleton for the TBeatNoteTimerState class +var BeatNoteTimerState : TBeatNoteTimerState; + +implementation + +uses + ULyrics, + UNote, + SysUtils, + UBeatNote, + Math; + +// Instantiate the singleton if necessary +procedure createTBeatNoteTimerState(); +begin + if BeatNoteTimerState = nil then + BeatNoteTimerState := TBeatNoteTimerState.Create(); +end; + +// Constructor with default values, with none of the players having a note playing +constructor TBeatNoteTimerState.Create; +var + count: integer; +begin + setLength(playerBeatNoteState, 12); // Maximum 12 players in any configuration + for count:= 0 to 11 do + begin + playerBeatNoteState[count].CurrentBeat:=-100; // usually current beat is positve or a few beats negative + playerBeatNoteState[count].LineFragment:=nil; + playerBeatNoteState[count].NoteHit:=false; + playerBeatNoteState[count].NoteHitExactBeatTime:=-100.0; // same as current beat, just this is float + end; + +end; + +// As a function of the present time in the song, determine the beat/beat silence notes playing +// for the different players (if any) +procedure TBeatNoteTimerState.analyzeBeatNoteTiming(Screen: TScreenSingController); +var + MaxCP, PlayerIndex: integer; + CP: integer; + J: cardinal; +begin + + MaxCP := 0; + if (CurrentSong.isDuet) and (PlayersPlay <> 1) then + MaxCP := 1; + + if (assigned(Screen)) then + begin + for J := 0 to MaxCP do + begin + CP := J; + + for PlayerIndex := 0 to PlayersPlay-1 do + begin + if (not CurrentSong.isDuet) or (PlayerIndex mod 2 = CP) then + begin + analyzeBeatNoteTimingForPlayer(CP); + end + end + end; + + end; + + end; + +// The idea here is to go through the song and see whether based on the current player and time, +// a beat not is currently open (also, not yet hit) +// The algorithm is as follows: First the beat notes are analyzed. If, within the +// present level of tolerance (set by the players difficulty level), a beat note is hit +// it is considered the note to be analyzed, regardless of potential overlap with a break. If +// several beat notes overlap with their tolerance bands, then, we choose the closest +// one to the actual timing. +// If none of the beat notes is presently playing (hit or not), the analysis proceeds with analyzing the breaks. +// Beat breaks are also expanded to the left (earlier times) such that no +// whole is created between a beat note and an adjacent break (in that order). + +procedure TBeatNoteTimerState.analyzeBeatNoteTimingForPlayer(CP: integer); +var + ActualBeat: real; + SentenceMin,SentenceMax,SentenceIndex, LineFragmentIndex: integer; + Line: PLine; + tolerance: real; // Time tolerance for beat detection + previousPlayerBeatNoteState: TPlayerBeatNoteState; // To see whether things have changed from last time + + CurrentLineFragment: PLineFragment; + noteFound: Boolean; + BestDeltaBeat: real; // The best timing delta between the current time (Actual Beat) and theoretical beat time + // This is to find out the best beat if several are overlapping with their tolerances +begin + noteFound:=false; + if CurrentSong.RapBeat then // This is only meant to work in the beat mode + begin + + + // Search through the song (for now) + SentenceMin := 0; + SentenceMax := High(Tracks[CP].Lines); + + // Directly look for the beat where we are + ActualBeat:=getActualBeatUsingBeatDetectionDelay(); + + tolerance:=BeatTolerance(CP); + + // Save the current state for comparison + previousPlayerBeatNoteState.CurrentBeat := playerBeatNoteState[CP].CurrentBeat; + previousPlayerBeatNoteState.LineFragment := playerBeatNoteState[CP].LineFragment; + previousPlayerBeatNoteState.NoteHit := playerBeatNoteState[CP].NoteHit; + previousPlayerBeatNoteState.NoteHitExactBeatTime := playerBeatNoteState[CP].NoteHitExactBeatTime; + previousPlayerBeatNoteState.NoteLastHitOnBeat := playerBeatNoteState[CP].NoteLastHitOnBeat; + + + + BestDeltaBeat:=0; + for SentenceIndex := SentenceMin to SentenceMax do + begin // The detection is done on the tracks (not the players detected notes) + // since we want to know which note should be hit by a beat + Line := @Tracks[CP].Lines[SentenceIndex]; + for LineFragmentIndex := 0 to Line.HighNote do + begin + CurrentLineFragment := @Line.Notes[LineFragmentIndex]; + if (CurrentLineFragment.NoteType= ntRap) then + begin + if spansCurrentBeat(ActualBeat, CurrentLineFragment.StartBeat, + CurrentLineFragment.NoteType, CurrentLineFragment.Duration, tolerance) then + begin + + if not noteFound then // First hit, take into account anyways + begin + noteFound:=true; + playerBeatNoteState[CP].LineFragment:=CurrentLineFragment; + playerBeatNoteState[CP].CurrentBeat:=beatInNote(ActualBeat, CurrentLineFragment.StartBeat, CurrentLineFragment.Duration); // Stay within the line fragment + BestDeltaBeat:=beatDistanceToNote(ActualBeat, CurrentLineFragment.StartBeat,CurrentLineFragment.Duration); + + if hitsCurrentBeat(ActualBeat, CurrentLineFragment.StartBeat, + CurrentLineFragment.NoteType, CurrentLineFragment.Duration, tolerance) then // Full-on hit, we fix this as the current note + begin + playerBeatNoteState[CP].CurrentBeat:=CurrentLineFragment.StartBeat; // Again, this is a full-on hit + break; // With a full hit, we are done, no more searching even if there would be overlaps + end; + // Note spanned, but not hit with the desired tolerance around the relevant (first) beat of the note + + + end + else + begin // We've already found beat notes that are technically hit, compare which is + // closer to the current timing + if beatDistanceToNote(ActualBeat, CurrentLineFragment.StartBeat,CurrentLineFragment.Duration) Date: Sat, 2 Oct 2021 23:17:21 +0200 Subject: [PATCH 3/6] Additions to drawing / animation --- src/base/UDraw.pas | 98 ++++++++++++++++++++++++++++---- src/base/UGraphic.pas | 13 +++++ src/base/UGraphicClasses.pas | 107 +++++++++++++++++++++++++++++++++-- 3 files changed, 203 insertions(+), 15 deletions(-) diff --git a/src/base/UDraw.pas b/src/base/UDraw.pas index 6a1fa0bb6..91649dbc7 100644 --- a/src/base/UDraw.pas +++ b/src/base/UDraw.pas @@ -115,7 +115,8 @@ implementation UScreenJukebox, USong, UTexture, - UWebcam; + UWebcam, + UBeatNote; procedure SingDrawWebCamFrame; @@ -445,7 +446,7 @@ procedure SingDrawLine(Left, Top, Right: real; Track, PlayerNumber: integer; Lin begin with Notes[Count] do begin - if NoteType <> ntFreestyle then + if (NoteType <> ntFreestyle) and ((NoteType <> ntRap) or not CurrentSong.RapBeat) then begin if Ini.EffectSing = 0 then // If Golden note Effect of then Change not Color @@ -541,6 +542,9 @@ procedure SingDrawLine(Left, Top, Right: real; Track, PlayerNumber: integer; Lin glDisable(GL_BLEND); glDisable(GL_TEXTURE_2D); end; + // Specific drawing of the beat notes (they are shifted -0.5 unit beats to be + // on the rythmic beats rather than between + SingDrawLineBeats(Left, Top, Right, Track, PlayerNumber, LineSpacing); end; // draw sung notes @@ -567,9 +571,12 @@ procedure SingDrawPlayerLine(X, Y, W: real; Track, PlayerIndex: integer; LineSpa else TempR := W / TempR; for N := 0 to Player[PlayerIndex].HighNote do + if (not CurrentSong.RapBeat) or (Player[PlayerIndex].Note[N].NoteType <>ntRap) then // the specific case of rap notes in beat mode is handled separately begin with Player[PlayerIndex].Note[N] do begin + + // Left part of note Rec.Left := X + (Start - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat) * TempR + 0.5 + 10*ScreenX; Rec.Right := Rec.Left + NotesW[PlayerIndex]; @@ -667,6 +674,7 @@ procedure SingDrawPlayerLine(X, Y, W: real; Track, PlayerIndex: integer; LineSpa GoldenRec.SavePerfectNotePos(Rec.Left, Rec.Top); end; end; + end; // with end; // for @@ -677,6 +685,9 @@ procedure SingDrawPlayerLine(X, Y, W: real; Track, PlayerIndex: integer; LineSpa GoldenRec.GoldenNoteTwinkle(Rec.Top,Rec.Bottom,Rec.Right, PlayerIndex); end; // if end; // if +// Specific drawing of the beat and beat silence notes hit by the player, + // with shift by 0.5 elementary beat units to be on the rythmic beats + SingDrawPlayerLineBeats(X, Y, W, Track, PlayerIndex, LineSpacing); end; //draw Note glow @@ -701,10 +712,11 @@ procedure SingDrawPlayerBGLine(Left, Top, Right: real; Track, PlayerIndex: integ with Tracks[Track].Lines[Tracks[Track].CurrentLine] do begin for Count := 0 to HighNote do + if (not CurrentSong.RapBeat) or (Notes[Count].NoteType <>ntRap) then begin with Notes[Count] do begin - if NoteType <> ntFreestyle then + if (NoteType <> ntFreestyle) then begin // begin: 14, 20 // easy: 6, 11 @@ -790,6 +802,10 @@ procedure SingDrawPlayerBGLine(Left, Top, Right: real; Track, PlayerIndex: integ glDisable(GL_BLEND); glDisable(GL_TEXTURE_2D); end; + + // Drawing of 0.5-shifted beat notes separately + SingDrawPlayerBGLineBeats(Left, Top, Right, Track, PlayerIndex, LineSpacing); + end; (** @@ -1831,7 +1847,11 @@ procedure EditDrawLine(X, YBaseNote, W, H: real; Track: integer; NumLines: integ ntFreestyle: glColor4f(1, 1, 1, 0.35); ntNormal: glColor4f(1, 1, 1, 0.85); ntGolden: Glcolor4f(1, 1, 0.3, 0.85); - ntRap: glColor4f(1, 1, 1, 0.85); + ntRap: + begin + if CurrentSong.RapBeat then + glColor4f(1, 1, 1, 0.15) else glColor4f(1, 1, 1, 0.85); + end; ntRapGolden: Glcolor4f(1, 1, 0.3, 0.85); end; // case @@ -1840,7 +1860,18 @@ procedure EditDrawLine(X, YBaseNote, W, H: real; Track: integer; NumLines: integ Rec.Right := Rec.Left + NotesW[0]; Rec.Top := YBaseNote - (Tone-BaseNote)*Space/2 - NotesH[0]; Rec.Bottom := Rec.Top + 2 * NotesH[0]; - If (NoteType = ntRap) or (NoteType = ntRapGolden) then + + // For beat notes, this needs to be shifted by 0.5 beats as they are centered on musical beats rather than the intervals + // between them + + if (NoteType = ntRap) and CurrentSong.RapBeat then + begin + Rec.Left := (StartBeat - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat-0.5) * TempR + X + 0.5 + 10*ScreenX; + Rec.Right := Rec.Left + NotesW[0]; + end; + + + If ((NoteType = ntRap) and not CurrentSong.RapBeat) or (NoteType = ntRapGolden) then begin glBindTexture(GL_TEXTURE_2D, Tex_Left_Rap[Color].TexNum); end @@ -1856,11 +1887,18 @@ procedure EditDrawLine(X, YBaseNote, W, H: real; Track: integer; NumLines: integ glEnd; GoldenStarPos := Rec.Left; - // middle part - Rec.Left := Rec.Right; - Rec.Right := (StartBeat + Duration - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat) * TempR + X - NotesW[0] - 0.5 + 10*ScreenX; + // middle part, with shift for beat notes - If (NoteType = ntRap) or (NoteType = ntRapGolden) then + if (NoteType = ntRap) and CurrentSong.RapBeat then + begin + Rec.Left := Rec.Right; + Rec.Right := (StartBeat + Duration - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat-0.5) * TempR + X - NotesW[0] - 0.5 + 10*ScreenX; + end else + begin + Rec.Left := Rec.Right; + Rec.Right := (StartBeat + Duration - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat) * TempR + X - NotesW[0] - 0.5 + 10*ScreenX; + end; + If ((NoteType = ntRap) and not CurrentSong.RapBeat) or (NoteType = ntRapGolden) then begin glBindTexture(GL_TEXTURE_2D, Tex_Mid_Rap[Color].TexNum); end @@ -1879,7 +1917,7 @@ procedure EditDrawLine(X, YBaseNote, W, H: real; Track: integer; NumLines: integ Rec.Left := Rec.Right; Rec.Right := Rec.Right + NotesW[0]; - If (NoteType = ntRap) or (NoteType = ntRapGolden) then + If ((NoteType = ntRap) and not CurrentSong.RapBeat) or (NoteType = ntRapGolden) then begin glBindTexture(GL_TEXTURE_2D, Tex_Right_Rap[Color].TexNum); end @@ -1898,7 +1936,45 @@ procedure EditDrawLine(X, YBaseNote, W, H: real; Track: integer; NumLines: integ begin GoldenRec.SaveGoldenStarsRec(GoldenStarPos, Rec.Top, Rec.Right, Rec.Bottom); end; - + + // Draw the beat stronger in rap beat mode + if (NoteType = ntRap) and CurrentSong.RapBeat then + begin + // The dot on the beat is drawn less transparent than the tail + glColor4f(1, 1, 1, 0.85); + Rec.Left := (StartBeat - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat-0.5) * TempR + X + 0.5 + 10*ScreenX; + Rec.Right := Rec.Left + NotesW[0]; + Rec.Top := YBaseNote - (Tone-BaseNote)*Space/2 - NotesH[0]; + Rec.Bottom := Rec.Top + 2 * NotesH[0]; + glBindTexture(GL_TEXTURE_2D, Tex_Left[Color].TexNum); + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(Rec.Left, Rec.Top); + glTexCoord2f(0, 1); glVertex2f(Rec.Left, Rec.Bottom); + glTexCoord2f(1, 1); glVertex2f(Rec.Right, Rec.Bottom); + glTexCoord2f(1, 0); glVertex2f(Rec.Right, Rec.Top); + glEnd; + // Middle, only for the beat at the beginning of the note (duration 1) + Rec.Left := Rec.Right; + Rec.Right := (StartBeat + 1 - Tracks[Track].Lines[Tracks[Track].CurrentLine].Notes[0].StartBeat-0.5) * TempR + X - NotesW[0] - 0.5 + 10*ScreenX; + glBindTexture(GL_TEXTURE_2D, Tex_Mid[Color].TexNum); + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(Rec.Left, Rec.Top); + glTexCoord2f(0, 1); glVertex2f(Rec.Left, Rec.Bottom); + glTexCoord2f(1, 1); glVertex2f(Rec.Right, Rec.Bottom); + glTexCoord2f(1, 0); glVertex2f(Rec.Right, Rec.Top); + glEnd; + // right, only for the beat + Rec.Left := Rec.Right; + Rec.Right := Rec.Right + NotesW[0]; + glBindTexture(GL_TEXTURE_2D, Tex_Right[Color].TexNum); + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(Rec.Left, Rec.Top); + glTexCoord2f(0, 1); glVertex2f(Rec.Left, Rec.Bottom); + glTexCoord2f(1, 1); glVertex2f(Rec.Right, Rec.Bottom); + glTexCoord2f(1, 0); glVertex2f(Rec.Right, Rec.Top); + glEnd; + end; + end; // with end; // for end; // with diff --git a/src/base/UGraphic.pas b/src/base/UGraphic.pas index 91881d606..35cf55dad 100644 --- a/src/base/UGraphic.pas +++ b/src/base/UGraphic.pas @@ -62,6 +62,8 @@ interface UScreenOptionsThemes, UScreenOptionsRecord, UScreenOptionsAdvanced, + UScreenOptionsBeatPlay, + UScreenOptionsBeatPlayPeakAnalysis, UScreenOptionsNetwork, UScreenOptionsWebcam, UScreenOptionsJukebox, @@ -159,6 +161,8 @@ TRecR = record ScreenOptionsThemes: TScreenOptionsThemes; ScreenOptionsRecord: TScreenOptionsRecord; ScreenOptionsAdvanced: TScreenOptionsAdvanced; + ScreenOptionsBeatPlay: TScreenOptionsBeatPlay; + ScreenOptionsBeatPlayPeakAnalysis: TScreenOptionsBeatPlayPeakAnalysis; ScreenOptionsNetwork: TScreenOptionsNetwork; ScreenOptionsWebcam: TScreenOptionsWebcam; ScreenOptionsJukebox: TScreenOptionsJukebox; @@ -213,6 +217,9 @@ TRecR = record Tex_plain_Mid: array[1..UIni.IMaxPlayerCount] of TTexture; //rename to tex_notebg_mid Tex_plain_Right: array[1..UIni.IMaxPlayerCount] of TTexture; //rename to tex_notebg_right + Tex_Note_Beat_BG: array[1..UIni.IMaxPlayerCount] of TTexture; //For showing beat with + Tex_Note_Beat: array[1..UIni.IMaxPlayerCount] of TTexture; // predefined duration of 1 unit length + Tex_BG_Left: array[1..UIni.IMaxPlayerCount] of TTexture; //rename to tex_noteglow_left Tex_BG_Mid: array[1..UIni.IMaxPlayerCount] of TTexture; //rename to tex_noteglow_mid Tex_BG_Right: array[1..UIni.IMaxPlayerCount] of TTexture; //rename to tex_noteglow_right @@ -232,6 +239,7 @@ TRecR = record Tex_Note_Star: TTexture; Tex_Note_Perfect_Star: TTexture; + Tex_Note_Clap: TTexture; // Clap sign Tex_Ball: TTexture; Tex_Lyric_Help_Bar: TTexture; @@ -389,6 +397,7 @@ procedure LoadTextures; Tex_Note_Perfect_Star := Texture.LoadTexture(Skin.GetTextureFileName('NotePerfectStar'), TEXTURE_TYPE_TRANSPARENT, 0); Tex_Note_Star := Texture.LoadTexture(Skin.GetTextureFileName('NoteStar') , TEXTURE_TYPE_TRANSPARENT, $FFFFFF); + Tex_Note_Clap := Texture.LoadTexture(Skin.GetTextureFileName('BeatClap') , TEXTURE_TYPE_TRANSPARENT, $FFFFFF); Tex_Ball := Texture.LoadTexture(Skin.GetTextureFileName('Ball'), TEXTURE_TYPE_TRANSPARENT, $FF00FF); Tex_Lyric_Help_Bar := Texture.LoadTexture(Skin.GetTextureFileName('LyricHelpBar'), TEXTURE_TYPE_TRANSPARENT, 0); @@ -959,6 +968,10 @@ procedure LoadScreens(Title: string); ScreenOptionsRecord := TScreenOptionsRecord.Create; SDL_SetWindowTitle(Screen, PChar(Title + ' - Loading ScreenOptionsAdvanced')); ScreenOptionsAdvanced := TScreenOptionsAdvanced.Create; + SDL_SetWindowTitle(Screen, PChar(Title + ' - Loading ScreenOptionsBeatPlay')); // Screen with options for beat tapping + ScreenOptionsBeatPlay := TScreenOptionsBeatPlay.Create; + SDL_SetWindowTitle(Screen, PChar(Title + ' - Loading ScreenOptionBeatPlayPeakAnalysis')); // Screen with audio detection options for tapping + ScreenOptionsBeatPlayPeakAnalysis := TScreenOptionsBeatPlayPeakAnalysis.Create; SDL_SetWindowTitle(Screen, PChar(Title + ' - Loading ScreenOptionsNetwork')); ScreenOptionsNetwork := TScreenOptionsNetwork.Create; SDL_SetWindowTitle(Screen, PChar(Title + ' - Loading ScreenOptionsWebCam')); diff --git a/src/base/UGraphicClasses.pas b/src/base/UGraphicClasses.pas index 7a82dd095..ca0683589 100644 --- a/src/base/UGraphicClasses.pas +++ b/src/base/UGraphicClasses.pas @@ -44,7 +44,7 @@ interface type - TParticleType = (GoldenNote, PerfectNote, NoteHitTwinkle, PerfectLineTwinkle, ColoredStar, Flare); + TParticleType = (GoldenNote, PerfectNote, NoteHitTwinkle, PerfectLineTwinkle, ColoredStar, Flare, PerfectBeat); TColour3f = record r, g, b: real; @@ -60,7 +60,7 @@ TParticle = class Tex : cardinal; //Tex num from Textur Manager Live : byte; //How many Cycles before Kill RecIndex : integer; //To which rectangle this particle belongs (only GoldenNote) - StarType : TParticleType; // GoldenNote | PerfectNote | NoteHitTwinkle | PerfectLineTwinkle + StarType : TParticleType; // GoldenNote | PerfectNote | NoteHitTwinkle | PerfectLineTwinkle | PerfectBeat Alpha : real; // used for fading... mX, mY : real; // movement-vector for PerfectLineTwinkle SizeMod : real; // experimental size modifier @@ -96,7 +96,7 @@ TEffectManager = class RecArray : array of RectanglePositions; TwinkleArray : array[0..UIni.IMaxPlayerCount-1] of real; // store x-position of last twinkle for every player PerfNoteArray : array of PerfectNotePositions; - + BeatLevel : integer; FlareTex: TTexture; constructor Create; @@ -110,14 +110,27 @@ TEffectManager = class StarType: TParticleType; Player: cardinal // for PerfectLineTwinkle ): cardinal; + + function SpawnAlternateTexNum(X, Y: real; + Screen: integer; + Live: byte; + StartFrame: integer; + RecArrayIndex: integer; // this is only used with GoldenNotes + StarType: TParticleType; + Player: cardinal; // for PerfectLineTwinkle + TexNum: cardinal // Supply a specific Texnum instead of the usual one + ): cardinal; procedure SpawnRec(); procedure Kill(index: cardinal); procedure KillAll(); procedure SentenceChange(CP: integer); procedure SaveGoldenStarsRec(Xtop, Ytop, Xbottom, Ybottom: real); procedure SavePerfectNotePos(Xtop, Ytop: real); + procedure SavePerfectBeatPos(Xtop, Ytop: real); procedure GoldenNoteTwinkle(Top, Bottom, Right: real; Player: integer); procedure SpawnPerfectLineTwinkle(); + procedure IncrementBeatLevel(); + procedure ResetBeatLevel(); end; var @@ -283,6 +296,18 @@ constructor TParticle.Create(cX, cY : real; Col[3].b := 1; end; + PerfectBeat: + begin + Tex := Tex_Score_Ratings[1].TexNum; + W := 30; + H := 30; + SetLength(Col,1); + Col[0].r := 1; + Col[0].g := 1; + Col[0].b := 1; + mX := 0; + mY := 0; + end; else // just some random default values begin Tex := Tex_Note_Star.TexNum; @@ -350,6 +375,10 @@ procedure TParticle.LiveOn; mY := mY+1.8; // mX := mX/2; end; + PerfectBeat: + begin + Alpha := (Live/10); + end; end; end; @@ -375,10 +404,22 @@ procedure TParticle.Draw; glColor4f(Col[L].r, Col[L].g, Col[L].b, Alpha); glBegin(GL_QUADS); - glTexCoord2f((1/16) * Frame, 0); glVertex2f(X-W*Scale[L]*SizeMod, Y-H*Scale[L]*SizeMod); + if StarType <> perfectBeat then + begin + glTexCoord2f((1/16) * Frame, 0); glVertex2f(X-W*Scale[L]*SizeMod, Y-H*Scale[L]*SizeMod); glTexCoord2f((1/16) * Frame + (1/16), 0); glVertex2f(X-W*Scale[L]*SizeMod, Y+H*Scale[L]*SizeMod); glTexCoord2f((1/16) * Frame + (1/16), 1); glVertex2f(X+W*Scale[L]*SizeMod, Y+H*Scale[L]*SizeMod); glTexCoord2f((1/16) * Frame, 1); glVertex2f(X+W*Scale[L]*SizeMod, Y-H*Scale[L]*SizeMod); + + end + else // For the beat smileys + begin + glTexCoord2f(0, 0); glVertex2f(X-W*Scale[L]*SizeMod, Y-H*Scale[L]*SizeMod); + glTexCoord2f(0, 1); glVertex2f(X-W*Scale[L]*SizeMod, Y+H*Scale[L]*SizeMod); + glTexCoord2f(1, 1); glVertex2f(X+W*Scale[L]*SizeMod, Y+H*Scale[L]*SizeMod); + glTexCoord2f(1, 0); glVertex2f(X+W*Scale[L]*SizeMod, Y-H*Scale[L]*SizeMod); + + end; glEnd; end; @@ -402,6 +443,7 @@ constructor TEffectManager.Create; begin TwinkleArray[c] := 0; end; + BeatLevel:=1; end; destructor TEffectManager.Destroy; @@ -457,6 +499,19 @@ function TEffectManager.Spawn(X, Y: real; Screen: integer; Live: byte; StartFram Particle[Result] := TParticle.Create(X, Y, Screen, Live, StartFrame, RecArrayIndex, StarType, Player); end; +// The idea here is to give additional flexibility in providing different textures with the same behaviour +// Specifically used for beat notes with different smileys appearing with increasing number of +// correct beat hits +function TEffectManager.SpawnAlternateTexNum(X, Y: real; Screen: integer; Live: byte; + StartFrame : integer; RecArrayIndex : integer; StarType : TParticleType; + Player: cardinal; TexNum: cardinal): cardinal; +begin + Result := Length(Particle); + SetLength(Particle, (Result + 1)); + Particle[Result] := TParticle.Create(X, Y, Screen, Live, StartFrame, RecArrayIndex, StarType, Player); + Particle[Result].Tex := TexNum; +end; + // manage Sparkling of GoldenNote Bars procedure TEffectManager.SpawnRec(); var @@ -629,6 +684,50 @@ procedure TEffectManager.SaveGoldenStarsRec(Xtop, Ytop, Xbottom, Ybottom: real); RecArray[NewIndex].Screen := ScreenAct; end; +procedure TEffectManager.SavePerfectBeatPos(Xtop, Ytop: real); +var + P : integer; // P like used in Positions + NewIndex : integer; + RandomFrame : integer; +begin + for P := 0 to high(PerfNoteArray) do // Do we already have that "new" position? + begin + with PerfNoteArray[P] do + if (ceil(xPos) = ceil(Xtop)) and (ceil(yPos) = ceil(Ytop)) and + (Screen = ScreenAct) then + exit; // it's already in the array, so we don't have to create a new one + end; //for + + // we got a new position, add the new positions to our array + NewIndex := Length(PerfNoteArray); + SetLength(PerfNoteArray, NewIndex + 1); + PerfNoteArray[NewIndex].xPos := Xtop; + PerfNoteArray[NewIndex].yPos := Ytop; + PerfNoteArray[NewIndex].Screen := ScreenAct; + // Launch the smiley animation + SpawnAlternateTexNum(ceil(Xtop), ceil(Ytop), ScreenAct, 32, 0, -1, + PerfectBeat, 0, Tex_Score_Ratings[BeatLevel].TexNum); + + + +end; +// This causes advancingly good smileys (the same as the score levels) +// to be applied with consecutively hit beat notes +procedure TEffectManager.IncrementBeatLevel(); +begin + if BeatLevel Date: Sat, 2 Oct 2021 23:30:16 +0200 Subject: [PATCH 4/6] Configuration screens --- src/beatNote/UScreenOptionsBeatPlay.pas | 266 ++++++ .../UScreenOptionsBeatPlayPeakAnalysis.pas | 886 ++++++++++++++++++ src/screens/UScreenOptions.pas | 10 + src/screens/UScreenOptionsRecord.pas | 8 +- 4 files changed, 1169 insertions(+), 1 deletion(-) create mode 100644 src/beatNote/UScreenOptionsBeatPlay.pas create mode 100644 src/beatNote/UScreenOptionsBeatPlayPeakAnalysis.pas diff --git a/src/beatNote/UScreenOptionsBeatPlay.pas b/src/beatNote/UScreenOptionsBeatPlay.pas new file mode 100644 index 000000000..e7f5da49d --- /dev/null +++ b/src/beatNote/UScreenOptionsBeatPlay.pas @@ -0,0 +1,266 @@ +{* 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 UScreenOptionsBeatPlay; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + UDisplay, + UFiles, + UIni, + UMenu, + UMusic, + UThemes, + sdl2; + +// Class definition for the options screen for the tapping (accessible through +// Tools -> Options -> Beat Tapping in the english version +type + TScreenOptionsBeatPlay = class(TMenu) + private + BeatDetectionDelayOptInt:integer; // Value for Keyboard delay. Private because we only store the float value which is 10x this + BeatDetectionDelaySelectNum: integer; // This is the reference number of the graphical element + ButtonConfigureID: integer; + public + constructor Create; override; + function ParseInput(PressedKey: cardinal; CharCode: UCS4Char; PressedDown: boolean): boolean; override; + procedure OnShow; override; + procedure UpdateCalculatedSelectSlides(Init: boolean); // For showing suitable text to choose + end; + + + +implementation + +uses + UGraphic, + UHelp, + ULog, + UUnicodeUtils, + SysUtils, + UCommon; + +type +TGetTextFunc = function(var Param: integer; Offset: integer; Modify: boolean; OptText: PUtf8String): boolean; +UTF8StringArray = array of UTF8String; + +function TScreenOptionsBeatPlay.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; PressedDown: boolean): 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 + Ini.Save; + AudioPlayback.PlaySound(SoundLib.Back); + FadeTo(@ScreenOptions); + end; + SDLK_TAB: + begin + ScreenPopupHelp.ShowPopup(); + end; + SDLK_RETURN: + begin + if SelInteraction = 2 then + begin + Ini.Save; + AudioPlayback.PlaySound(SoundLib.Back); + FadeTo(@ScreenOptions); + end; + if SelInteraction = 3 then + begin + Ini.Save; + FadeTo(@ScreenOptionsBeatPlayPeakAnalysis); + end; + end; + SDLK_DOWN: + InteractNext; + SDLK_UP : + InteractPrev; + SDLK_RIGHT: + begin + if (SelInteraction >= 0) and (SelInteraction <= 4) then + begin + AudioPlayback.PlaySound(SoundLib.Option); + InteractInc; + end; + UpdateCalculatedSelectSlides(false); + end; + SDLK_LEFT: + begin + if (SelInteraction >= 0) and (SelInteraction <= 4) then + begin + AudioPlayback.PlaySound(SoundLib.Option); + InteractDec; + end; + UpdateCalculatedSelectSlides(false); + end; + end; + end; +end; + +constructor TScreenOptionsBeatPlay.Create; +begin + inherited Create; + + LoadFromTheme(Theme.OptionsBeatPlay); + + Theme.OptionsBeatPlay.SelectBeatDetectionDelay.oneItemOnly := true; + Theme.OptionsBeatPlay.SelectBeatDetectionDelay.showArrows := true; + + + Theme.OptionsBeatPlay.SelectBeatPlayClapSign.showArrows := true; + Theme.OptionsBeatPlay.SelectBeatPlayClapSign.oneItemOnly := true; + AddSelectSlide(Theme.OptionsBeatPlay.SelectBeatPlayClapSign, Ini.BeatPlayClapSignOn, IBeatPlayClapSignOn); + + UpdateCalculatedSelectSlides(true); // Instantiate the calculated slides + + AddButton(Theme.OptionsAdvanced.ButtonExit); + if (Length(Button[0].Text)=0) then + AddButtonText(20, 5, Theme.Options.Description[OPTIONS_DESC_INDEX_BACK]); + + AddButton(Theme.OptionsBeatPlay.ButtonAudioConfigure); + if (Length(Button[1].Text)=0) then + AddButtonText(20, 5, Theme.OptionsBeatPlay.Description[0]); + + + + + + + + Interaction := 0; + + + +end; + +procedure TScreenOptionsBeatPlay.OnShow; +begin + inherited; + + Interaction := 0; + +end; + +function GetBeatDetectionDelayOptText(var Param: integer; Offset: integer; Modify: boolean; OptText: PUTF8String): boolean; +begin + if (Param + Offset * 10 < -1000) or (Param + Offset * 10 > 1000) then + Result := false + else + begin + if OptText <> nil then + OptText^ := Format('%d ms', [Param + Offset * 10]); + if Modify then + Param := Param + Offset * 10; + Result := true; + end; +end; + +function GetBeatChoiceOptText(var Param: integer; Offset: integer; Modify: boolean; OptText: PUTF8String): boolean; +begin + if (Param + Offset * 10 < -1000) or (Param + Offset * 10 > 1000) then + Result := false + else + begin + if OptText <> nil then + OptText^ := Format('%d ms', [Param + Offset * 10]); + if Modify then + Param := Param + Offset * 10; + Result := true; + end; +end; + + +procedure CalculateSelectSlide(Init: boolean; GetText: TGetTextFunc; var Param: integer; var OptInt: integer; var Texts: UTF8StringArray); +var + Idx: integer; + NumOpts: integer; +begin + if not GetText(Param, 0, true, nil) then + begin + SetLength(Texts, 0); + Exit; + end; + if GetText(Param, -1, false, nil) then + Idx := 1 + else + Idx := 0; + if not Init then + begin + if OptInt = Idx then + Exit; + GetText(Param, OptInt - Idx, true, nil); + if GetText(Param, -1, false, nil) then + Idx := 1 + else + Idx := 0; + end; + OptInt := Idx; + if GetText(Param, 1, false, nil) then + NumOpts := Idx + 2 + else + NumOpts := Idx + 1; + SetLength(Texts, NumOpts); + for Idx := 0 to High(Texts) do + GetText(Param, Idx - OptInt, false, @Texts[Idx]); +end; + +procedure TScreenOptionsBeatPlay.UpdateCalculatedSelectSlides(Init: boolean); +begin + CalculateSelectSlide(Init, @GetBeatDetectionDelayOptText, Ini.BeatDetectionDelay, BeatDetectionDelayOptInt, IBeatDetectionDelay); + if Init then + begin + BeatDetectionDelaySelectNum := AddSelectSlide(Theme.OptionsBeatPlay.SelectBeatDetectionDelay, BeatDetectionDelayOptInt, IBeatDetectionDelay); + end + else + begin + UpdateSelectSlideOptions(Theme.OptionsBeatPlay.SelectBeatDetectionDelay, BeatDetectionDelaySelectNum, IBeatDetectionDelay, BeatDetectionDelayOptInt); + end; +end; + + + + +end. diff --git a/src/beatNote/UScreenOptionsBeatPlayPeakAnalysis.pas b/src/beatNote/UScreenOptionsBeatPlayPeakAnalysis.pas new file mode 100644 index 000000000..c8e3dc1de --- /dev/null +++ b/src/beatNote/UScreenOptionsBeatPlayPeakAnalysis.pas @@ -0,0 +1,886 @@ +{* 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 UScreenOptionsBeatPlayPeakAnalysis; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + UDisplay, + UFiles, + UIni, + UMenu, + UMusic, + UThemes, + URecord, + sdl2, + UBeatNote; + + + + +type + +TBeatPeakInfo = record + BufferIndex: integer; // Detected onset + BufferIndexLast: integer; // Last point considered to be part of the peak + Volume: single; // Fraction of to full scale + RiseRateMillisecond: single; // Rise rate in units of fraction of full scale, per ms + MinPeakMillisecond: single; // Estimation of the peak duration in milliseconds + DropAfterPeak: single; // Drop after the peak in full scale units + PeakValid: boolean; + end; + +// Class definition for the options screen for the tapping (accessible through +// Tools -> Options -> Beat Tapping in the english version + TScreenOptionsBeatPlayPeakAnalysis = class(TMenu) + private + // current input device + CurrentDeviceIndex: integer; + PreviewDeviceIndex: integer; + CurrentChannel: array of integer; + + // string arrays for select-slide options + InputSourceNames: array of UTF8String; + InputDeviceNames: array of UTF8String; + SelectChannelOptions: array of UTF8String; + + // indices for widget-updates + SelectInputSourceID: integer; + SelectChannelID: integer; + SelectIntensityID: integer; + SelectMicBoostID: integer; + //SelectRiseRateID: integer; not used + //SelectMinPeakID: integer; not used + //SelectFallRateID: integer; not used + + PreviewChannel: TCaptureBuffer; // For showing the current sound trace + + peakDetected: Boolean; // True when we saw a peak + peakDetectionTime : cardinal; + peakInfo: TBeatPeakInfo; + + AnalysisBufferAtPeakDetection: array of smallint; + + // interaction IDs + ExitButtonIID: integer; + + public + constructor Create; override; + function ParseInput(PressedKey: cardinal; CharCode: UCS4Char; PressedDown: boolean): boolean; override; + procedure OnShow; override; + procedure onHide; override; + function Draw: boolean; override; + procedure UpdateInputDevice; + + procedure StartPreview; + procedure StopPreview; + + procedure doBeatDetection; // With the current parameters, try beat detection on running recording + + procedure BeatDrawOscilloscope(X, Y, W, H: real); + + end; + +function AnalyzeBufferBeatDetails(AnalysisBuffer: array of smallint; AudioFormat: TAudioFormatInfo; + timeBack: real; Threshold: integer; RiseRate: integer; MinPeakDuration: integer; + DropAfterPeak: integer; TestTimeAfterPeak: integer): TBeatPeakInfo; + + +implementation + +uses + UGraphic, + UHelp, + ULog, + UUnicodeUtils, + SysUtils, + dglOpenGL, + UCommon, + TextGL; + +type +TGetTextFunc = function(var Param: integer; Offset: integer; Modify: boolean; OptText: PUtf8String): boolean; +UTF8StringArray = array of UTF8String; + +function TScreenOptionsBeatPlayPeakAnalysis.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; PressedDown: boolean): 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 + Ini.Save; + AudioPlayback.PlaySound(SoundLib.Back); + FadeTo(@ScreenOptionsBeatPlay); + end; + SDLK_TAB: + begin + ScreenPopupHelp.ShowPopup(); + end; + SDLK_RETURN: + begin + if SelInteraction = 7 then + begin + //Ini.Save; + AudioPlayback.PlaySound(SoundLib.Back); + FadeTo(@ScreenOptionsBeatPlay); + end; + end; + SDLK_DOWN: + InteractNext; + SDLK_UP : + InteractPrev; + SDLK_RIGHT: + begin + if (SelInteraction >= 0) and (SelInteraction < 7) then + begin + AudioPlayback.PlaySound(SoundLib.Option); + InteractInc; + end; + UpdateInputDevice; + end; + SDLK_LEFT: + begin + if (SelInteraction >= 0) and (SelInteraction < 7) then + begin + AudioPlayback.PlaySound(SoundLib.Option); + InteractDec; + end; + UpdateInputDevice; + end; + end; + +end; + + + +end; + +constructor TScreenOptionsBeatPlayPeakAnalysis.Create; +var + DeviceIndex: integer; + SourceIndex: integer; + ChannelIndex: integer; + + InputDevice: TAudioInputDevice; + InputDeviceCfg: PInputDeviceConfig; + WidgetYPos: integer; + +begin + inherited Create; + + LoadFromTheme(Theme.OptionsBeatDetect); + + // set CurrentDeviceIndex to a valid device + if (Length(AudioInputProcessor.DeviceList) > 0) then + CurrentDeviceIndex := 0 + else + CurrentDeviceIndex := -1; + + PreviewDeviceIndex := -1; + + peakDetected:=false; // For the visualization by the oscilloscope + peakDetectionTime:=0; + + + + // init sliders if at least one device was detected + if (Length(AudioInputProcessor.DeviceList) > 0) then + begin + SetLength(CurrentChannel, High(AudioInputProcessor.DeviceList)+1); + for DeviceIndex:=0 to High(AudioInputProcessor.DeviceList) do + CurrentChannel[DeviceIndex]:=0; + + InputDevice := AudioInputProcessor.DeviceList[CurrentDeviceIndex]; + InputDeviceCfg := @Ini.InputDeviceConfig[InputDevice.CfgIndex]; + + // init device-selection slider + SetLength(InputDeviceNames, Length(AudioInputProcessor.DeviceList)); + for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do + begin + InputDeviceNames[DeviceIndex] := AudioInputProcessor.DeviceList[DeviceIndex].Name; + end; + // add device-selection slider (InteractionID: 0) + Theme.OptionsBeatDetect.SelectSlideCard.showArrows := true; + Theme.OptionsBeatDetect.SelectSlideCard.oneItemOnly := true; + AddSelectSlide(Theme.OptionsBeatDetect.SelectSlideCard, CurrentDeviceIndex, InputDeviceNames); + + // init source-selection slider + SetLength(InputSourceNames, Length(InputDevice.Source)); + for SourceIndex := 0 to High(InputDevice.Source) do + begin + InputSourceNames[SourceIndex] := InputDevice.Source[SourceIndex].Name; + end; + + Theme.OptionsBeatDetect.SelectSlideInput.showArrows := true; + Theme.OptionsBeatDetect.SelectSlideInput.oneItemOnly := true; + // add source-selection slider (InteractionID: 1) + SelectInputSourceID := AddSelectSlide(Theme.OptionsBeatDetect.SelectSlideInput, + InputDeviceCfg.Input, InputSourceNames); + + // compute list of selectable channels + SetLength(SelectChannelOptions, InputDevice.AudioFormat.Channels); + for ChannelIndex := 0 to InputDevice.AudioFormat.Channels-1 do + begin + SelectChannelOptions[ChannelIndex] := IntToStr(ChannelIndex + 1); + end; + + + Theme.OptionsBeatDetect.SelectChannel.showArrows := true; + Theme.OptionsBeatDetect.SelectChannel.oneItemOnly := true; + SelectChannelID := AddSelectSlide(Theme.OptionsBeatDetect.SelectChannel, + CurrentChannel[CurrentDeviceIndex], SelectChannelOptions); + + + + Theme.OptionsBeatDetect.SelectIntensityThreshold.showArrows := true; + Theme.OptionsBeatDetect.SelectIntensityThreshold.oneItemOnly := true; + SelectIntensityID := AddSelectSlide(Theme.OptionsBeatDetect.SelectIntensityThreshold, + Ini.InputDeviceBeatDetectionConfig[0].ChannelBeatDectectionSettings[0].IntensityThreshold, IBeatDetectIntensityThreshold); + + + Theme.OptionsBeatDetect.SelectMicBoost.showArrows := true; + Theme.OptionsBeatDetect.SelectMicBoost.oneItemOnly := true; + + SelectMicBoostID:= AddSelectSlide(Theme.OptionsBeatDetect.SelectMicBoost, Ini.MicBoost, IMicBoostTranslated); + + + + end; + + + + + + + ExitButtonIID:=AddButton(Theme.OptionsBeatDetect.ButtonExit); + if (Length(Button[0].Text)=0) then + AddButtonText(20, 5, Theme.Options.Description[OPTIONS_DESC_INDEX_BACK]); + + + + Interaction := 0; + + + +end; + + +procedure TScreenOptionsBeatPlayPeakAnalysis.OnShow; +begin + inherited; + + Interaction := 0; + + // create preview sound-buffer + PreviewChannel := TCaptureBuffer.Create(); + + UpdateInputDevice(); +end; + +procedure TScreenOptionsBeatPlayPeakAnalysis.OnHide; +begin + StopPreview(); + + // free preview buffer + PreviewChannel.Free; +end; + + + +procedure TScreenOptionsBeatPlayPeakAnalysis.UpdateInputDevice; +var + SourceIndex: integer; + InputDevice: TAudioInputDevice; + InputDeviceCfg: PInputDeviceConfig; + ChannelIndex: integer; +begin + //Log.LogStatus('Update input-device', 'TScreenOptionsRecord.UpdateCard') ; + + StopPreview(); + + // set CurrentDeviceIndex to a valid device + if (CurrentDeviceIndex > High(AudioInputProcessor.DeviceList)) then + CurrentDeviceIndex := 0; + + + + // update sliders if at least one device was detected + if (Length(AudioInputProcessor.DeviceList) > 0) then + begin + InputDevice := AudioInputProcessor.DeviceList[CurrentDeviceIndex]; + InputDeviceCfg := @Ini.InputDeviceConfig[InputDevice.CfgIndex]; + + // update source-selection slider + SetLength(InputSourceNames, Length(InputDevice.Source)); + for SourceIndex := 0 to High(InputDevice.Source) do + begin + InputSourceNames[SourceIndex] := InputDevice.Source[SourceIndex].Name; + end; + UpdateSelectSlideOptions(Theme.OptionsRecord.SelectSlideInput, SelectInputSourceID, + InputSourceNames, InputDeviceCfg.Input); + // update channel + SetLength(SelectChannelOptions, InputDevice.AudioFormat.Channels); + for ChannelIndex := 0 to InputDevice.AudioFormat.Channels-1 do + begin + SelectChannelOptions[ChannelIndex] := IntToStr(ChannelIndex+1); + end; + UpdateSelectSlideOptions(Theme.OptionsRecord.SelectChannel, + SelectChannelID, SelectChannelOptions, + CurrentChannel[CurrentDeviceIndex]); + + ChannelIndex := CurrentChannel[CurrentDeviceIndex]; + + + + // update the intensity threshold field + UpdateSelectSlideOptions(Theme.OptionsBeatDetect.SelectIntensityThreshold, + SelectIntensityID, IBeatDetectIntensityThreshold, Ini.InputDeviceBeatDetectionConfig[InputDeviceCfg.Input]. + ChannelBeatDectectionSettings[ChannelIndex].IntensityThreshold); + + // update the mic boost field + UpdateSelectSlideOptions(Theme.OptionsBeatDetect.SelectMicBoost, + SelectMicBoostID, IMicBoostTranslated, Ini.MicBoost); + + + + end; + + StartPreview(); +end; + +procedure TScreenOptionsBeatPlayPeakAnalysis.StartPreview; +var + Device: TAudioInputDevice; +begin + if ((CurrentDeviceIndex >= 0) and + (CurrentDeviceIndex <= High(AudioInputProcessor.DeviceList))) then + begin + Device := AudioInputProcessor.DeviceList[CurrentDeviceIndex]; + + // set preview channel as active capture channel + PreviewChannel.Clear(); + Device.LinkCaptureBuffer(CurrentChannel[CurrentDeviceIndex], PreviewChannel); + Device.Start(); + PreviewDeviceIndex := CurrentDeviceIndex; + + + + end; +end; + + +procedure TScreenOptionsBeatPlayPeakAnalysis.doBeatDetection; +var + BeatDetectionParams: TBeatDetectionParameters; + SampleIndex: integer; +begin + if peakDetected then + begin + if SDL_GetTicks()>peakDetectionTime+2000 then + begin + peakDetected := false; + peakDetectionTime:=0; + for SampleIndex:=0 to High(AnalysisBufferAtPeakDetection) do + AnalysisBufferAtPeakDetection[SampleIndex]:=0; + end; + end + else + begin + + BeatDetectionParams:=BeatDetectionParametersForDeviceAndChannel(CurrentDeviceIndex,CurrentChannel[CurrentDeviceIndex]); + + PreviewChannel.AnalyzeBufferBeatOnly(0.1,BeatDetectionParams.Threshold, + BeatDetectionParams.RiseRate, BeatDetectionParams.MinPeakDuration, BeatDetectionParams.DropAfterPeak, + BeatDetectionParams.TestTimeAfterPeak); + + if PreviewChannel.ToneValid then + begin + peakDetectionTime:=SDL_GetTicks(); + peakDetected:=true; + setLength(AnalysisBufferAtPeakDetection,High(PreviewChannel.AnalysisBuffer)+1); + for SampleIndex:=0 to High(AnalysisBufferAtPeakDetection) do + begin + AnalysisBufferAtPeakDetection[SampleIndex]:=PreviewChannel.AnalysisBuffer[SampleIndex]; + end; + + peakInfo:=AnalyzeBufferBeatDetails(AnalysisBufferAtPeakDetection, PreviewChannel.AudioFormat, + 0.1,BeatDetectionParams.Threshold, + BeatDetectionParams.RiseRate, BeatDetectionParams.MinPeakDuration, BeatDetectionParams.DropAfterPeak, + BeatDetectionParams.TestTimeAfterPeak); + + + + + end; + + end; // End detection of new peak +end; + +function TScreenOptionsBeatPlayPeakAnalysis.Draw: boolean; +begin + DrawBG; + DrawFG; + + doBeatDetection; + + BeatDrawOscilloscope(Theme.OptionsBeatDetect.ButtonExit.X+Theme.OptionsBeatDetect.ButtonExit.W+20, + Theme.OptionsBeatDetect.ButtonExit.Y-50, + Theme.OptionsBeatDetect.ButtonExit.W, 100); + + Result := true; +end; + +procedure TScreenOptionsBeatPlayPeakAnalysis.BeatDrawOscilloscope(X, Y, W, H: real); +var + SampleIndex: integer; + + MaxX, MaxY: real; + Col: TRGB; + +begin + + Col.R:=255; + Col.G:=255; + Col.B:=255; + + MaxX := W-1; + MaxY := (H-1) / 2; + + + glColor3f(0, 0, 0.2); + glBegin(GL_QUADS); + glVertex2f(X , Y); + glVertex2f(X , Y+2*MaxY); + glVertex2f(X+MaxX , Y+2*MaxY); + glVertex2f(X+MaxX , Y); + glEnd; + + + + glColor3f(Col.R, Col.G, Col.B); + + + + + glBegin(GL_LINE_STRIP); + glVertex2f(X , Y); + glVertex2f(X , Y+2*MaxY); + glVertex2f(X+MaxX , Y+2*MaxY); + glVertex2f(X+MaxX , Y); + glVertex2f(X , Y); + + glEnd; + + + + if peakDetected then + begin + + Col.R:=255; + Col.G:=255; + Col.B:=255; + + glColor3f(Col.R, Col.G, Col.B); + + + glBegin(GL_LINE_STRIP); + for SampleIndex := 0 to High(AnalysisBufferAtPeakDetection) do + begin + if (SampleIndex>=peakInfo.BufferIndex) and (SampleIndex<=peakInfo.BufferIndexLast) then + begin + Col.R:=255; + Col.G:=0; + Col.B:=0; + + glColor3f(Col.R, Col.G, Col.B); + end else + begin + Col.R:=255; + Col.G:=255; + Col.B:=255; + + glColor3f(Col.R, Col.G, Col.B); + end; + glVertex2f(X + MaxX * SampleIndex/High(AnalysisBufferAtPeakDetection), + Y + MaxY * (1 - AnalysisBufferAtPeakDetection[SampleIndex]/-Low(Smallint))); + end; + glEnd; + + + + end + else + begin + + Col.R:=255; + Col.G:=255; + Col.B:=255; + + glColor3f(Col.R, Col.G, Col.B); +{ + if (ParamStr(1) = '-black') or (ParamStr(1) = '-fsblack') then + glColor3f(1, 1, 1); +} + + + PreviewChannel.LockAnalysisBuffer(); + + + + glBegin(GL_LINE_STRIP); + for SampleIndex := 0 to High(PreviewChannel.AnalysisBuffer) do + begin + glVertex2f(X + MaxX * SampleIndex/High(PreviewChannel.AnalysisBuffer), + Y + MaxY * (1 - PreviewChannel.AnalysisBuffer[SampleIndex]/-Low(Smallint))); + end; + glEnd; + + PreviewChannel.UnlockAnalysisBuffer(); + + end; // end drawing oscilloscope line without peak +end; + +// Preview as in UScreenOptionsRecord.pas, except for drawing the oscilloscope line instead of volume and pitch +procedure TScreenOptionsBeatPlayPeakAnalysis.StopPreview; +var + ChannelIndex: integer; + Device: TAudioInputDevice; +begin + if ((PreviewDeviceIndex >= 0) and + (PreviewDeviceIndex <= High(AudioInputProcessor.DeviceList))) then + begin + Device := AudioInputProcessor.DeviceList[PreviewDeviceIndex]; + Device.Stop; + for ChannelIndex := 0 to High(Device.CaptureChannel) do + Device.LinkCaptureBuffer(ChannelIndex, nil); + end; + PreviewDeviceIndex := -1; + peakDetected:=false; +end; + + +function AnalyzeBufferBeatDetails(AnalysisBuffer: array of smallint; AudioFormat: TAudioFormatInfo; + timeBack: real; Threshold: integer; RiseRate: integer; MinPeakDuration: integer; + DropAfterPeak: integer; TestTimeAfterPeak: integer): TBeatPeakInfo; +var + + + ToneValid: boolean; // not really used but to have the function compatible with the code in URecord, AnalyzeBufferBeatOnly + Tone: integer; // not really used but to have the function compatible with the code in URecord, AnalyzeBufferBeatOnly + ToneAbs: integer; // not really used but to have the function compatible with the code in URecord, AnalyzeBufferBeatOnly + + Volume: single; + MaxVolume: single; + MeanVolume: single; + SampleIndex, SampleInterval, SampleIndexPeak, StartSampleIndex: integer; + BaselineStart, BaselineInterval, BaselineSampleIndex: integer; // To compare the peak sample values to baseline + HighestMaxIndex: integer; // The largest index above threshold, to define a peak length + SampleLowerLimit, SampleUpperLimit: integer; + detected, maximumdetected: Boolean; + RMSVolume, RMSVolumeBaseline, riserate_evaluated: single; + // Four potential criteria for a succesful beat note detetion (this is to discriminate against background noise) + passesThreshold: Boolean; // 1) Passing an absolute sound intensity threshold (anyways mandatory) + passesRiseRate: Boolean; // 2) Sufficiently quick rise rate (depending on user configuration in Ini variable) + passesDuration: Boolean; // 3) Sufficient duration (not just single off measurement, depends on configuration in Ini variable) + passesDropAfterPeak: Boolean; // 4) Sufficient quick fall after peak (depending on user configuration in Ini variable) + + DefaultreturnVal: TBeatPeakInfo; // Values to return if we do not find a peak + returnVal: TBeatPeakInfo; + +begin + + + + + + DefaultreturnVal.BufferIndex:=-1; + DefaultreturnVal.BufferIndexLast:=-1; + DefaultreturnVal.Volume:=0; + DefaultreturnVal.RiseRateMillisecond:=0; + DefaultreturnVal.MinPeakMillisecond:=0; + DefaultreturnVal.DropAfterPeak:=0; + DefaultreturnVal.PeakValid:=false; + + returnVal.BufferIndex:=-1; + returnVal.BufferIndexLast:=-1; + returnVal.Volume:=0; + returnVal.RiseRateMillisecond:=0; + returnVal.MinPeakMillisecond:=0; + returnVal.DropAfterPeak:=0; + returnVal.PeakValid:=false; + + + + ToneValid := false; + ToneAbs := -1; + Tone := -1; + detected := false; + + + passesThreshold:=false; + passesRiseRate:=false; + passesDuration:=false; + passesDropAfterPeak:=false; + + if RiseRate = 0 then + passesRiseRate := true; // No rise rate requirement, so passes anyways + + if MinPeakDuration = 0 then + passesDuration := true; // No minimal duration, so test passed anyways + + if DropAfterPeak =0 then + passesDropAfterPeak:= true; + + + + StartSampleIndex:=High(AnalysisBuffer)-Round(timeBack*AudioFormat.SampleRate); + + if(StartSampleIndex < 0) then + StartSampleIndex := 0; + + for SampleIndex := StartSampleIndex to High(AnalysisBuffer) do + begin + + passesThreshold:=false; + passesRiseRate:=false; + passesDuration:=false; + passesDropAfterPeak:=false; + + if RiseRate = 0 then + passesRiseRate := true; // No rise rate requirement, so passes anyways + + if MinPeakDuration = 0 then + passesDuration := true; // No minimal duration, so test passed anyways + + if DropAfterPeak =0 then + passesDropAfterPeak:= true; + + Volume := Abs(AnalysisBuffer[SampleIndex]) / (-Low(smallint)) *100; + if Volume > Threshold then + begin + passesThreshold:=true; + end; + + returnVal.BufferIndex:=SampleIndex; + returnVal.Volume:=Volume; + + // Before going further, check whether by any chance we already pass all criteria + if passesThreshold and passesRiseRate and passesDuration and passesDropAfterPeak then + begin + detected:=true; + + Break; + end; + + // First test passed, check for rise rate if necessary + if passesThreshold and (not passesRiseRate) then begin + BaselineStart:=SampleIndex-Round(0.005*AudioFormat.SampleRate); + if BaselineStart >= Low(AnalysisBuffer) then + begin + BaselineInterval:=Round(0.004*AudioFormat.SampleRate); + RMSVolumeBaseline:=0; + for BaselineSampleIndex := BaselineStart to BaselineStart+BaselineInterval do + begin + RMSVolumeBaseline := + RMSVolumeBaseline+(AnalysisBuffer[BaselineSampleIndex])*(AnalysisBuffer[BaselineSampleIndex])/(-Low(smallint))/(-Low(smallint)); + end; + RMSVolumeBaseline:=Sqrt(RMSVolumeBaseline/(BaselineInterval+1)); + + BaselineStart:=SampleIndex; + BaselineInterval:=Round(0.003*AudioFormat.SampleRate); + if BaselineStart+BaselineInterval <= High(AnalysisBuffer) then + begin + + RMSVolume:=0; + for BaselineSampleIndex := BaselineStart to BaselineStart+BaselineInterval do + begin + RMSVolume := + RMSVolume+(AnalysisBuffer[BaselineSampleIndex])*(AnalysisBuffer[BaselineSampleIndex])/(-Low(smallint))/(-Low(smallint)); + end; + RMSVolume:=Sqrt(RMSVolume/(BaselineInterval+1)); + + riserate_evaluated:=(RMSVolume-RMSVolumeBaseline)*100; + // The idea is that we want to have quick rise but then also something that stays a bit constant or continues to rise + if (riserate_evaluated>=RiseRate) and (RMSVolume*100.0 > Volume/2.0) then + begin + passesRiseRate:=true; + end; + + end; // End we can get the peak RMS + end; + + end; + + returnVal.RiseRateMillisecond:=riserate_evaluated; + + // Again, check whether by any chance we already pass all criteria + if passesThreshold and passesRiseRate and passesDuration and passesDropAfterPeak then + begin + + + detected:=true; + Break; + end; + + // First two tests OK, but not (yet) the third one + if passesThreshold and passesRiseRate and (not passesDuration) then + begin + SampleUpperLimit:=SampleIndex+Round(0.001*AudioFormat.SampleRate*MinPeakDuration*2); + if SampleUpperLimit > High(AnalysisBuffer) then + SampleUpperLimit := High(AnalysisBuffer); + SampleLowerLimit:=SampleIndex+Round(0.001*AudioFormat.SampleRate*MinPeakDuration); + if SampleLowerLimit > High(AnalysisBuffer) then + SampleLowerLimit := High(AnalysisBuffer); + maximumdetected:=false; + HighestMaxIndex:=-1; + for BaselineSampleIndex := SampleLowerLimit to SampleUpperLimit do + begin + if Abs(AnalysisBuffer[BaselineSampleIndex]) / (-Low(smallint)) *100 > Threshold then + begin + maximumdetected:=true; + HighestMaxIndex:= BaselineSampleIndex; + end; + end; + if maximumdetected then begin + passesDuration:=true; + end; + + end; + + returnVal.MinPeakMillisecond:=(HighestMaxIndex-SampleIndex)/AudioFormat.SampleRate*1000.0; + returnVal.BufferIndexLast:=HighestMaxIndex; + + // Again, check whether by any chance we already pass all criteria + if passesThreshold and passesRiseRate and passesDuration and passesDropAfterPeak then + begin + detected:=true; + Break; + end; + + // + if passesThreshold and passesRiseRate and passesDuration and (not passesDropAfterPeak) then + begin + + + BaselineStart:=SampleIndex; + BaselineInterval:=Round(0.003*AudioFormat.SampleRate); + if BaselineStart+BaselineInterval <= High(AnalysisBuffer) then + begin + + RMSVolumeBaseline:=0; + for BaselineSampleIndex := BaselineStart to BaselineStart+BaselineInterval do + begin + RMSVolumeBaseline := + RMSVolumeBaseline+(AnalysisBuffer[BaselineSampleIndex])*(AnalysisBuffer[BaselineSampleIndex])/(-Low(smallint))/(-Low(smallint)); + end; + RMSVolumeBaseline:=Sqrt(RMSVolumeBaseline/(BaselineInterval+1)); + SampleUpperLimit:=SampleIndex+Round((TestTimeAfterPeak/1000.0+0.005)*AudioFormat.SampleRate); + SampleLowerLimit:=SampleIndex+Round(TestTimeAfterPeak/1000.0*AudioFormat.SampleRate); + // Avoid indexing error by accessing non-existent points + if SampleUpperLimit <= High(AnalysisBuffer) then + begin + RMSVolume:=0; + for BaselineSampleIndex := SampleLowerLimit to SampleUpperLimit do + begin + RMSVolume:=RMSVolume+(AnalysisBuffer[BaselineSampleIndex])*(AnalysisBuffer[BaselineSampleIndex])/(-Low(smallint))/(-Low(smallint)); + end; + RMSVolume:=Sqrt(RMSVolume/(SampleUpperLimit-SampleLowerLimit+1)); + + + + // Fall rate is relative to max peak intensity (here, RMSVolumeBaseline taken on 1ms peak from initial detection on) + if ((RMSVolumeBaseline - RMSVolume)/RMSVolumeBaseline)*100.0 >= DropAfterPeak then + begin + passesDropAfterPeak:=true; + end; + end; + + returnVal.DropAfterPeak:=(RMSVolumeBaseline - RMSVolume)/RMSVolumeBaseline; + + end; + + + + end; + + // Final test if everything passes + if passesThreshold and passesRiseRate and passesDuration and passesDropAfterPeak then + begin + detected:=true; + Break; + end; + + + + + end; + + + + + + + + if detected then + begin + Result:=returnVal; + ToneValid := true; + ToneAbs:=48; + Tone:=0; + + + + end else + begin + Result:=defaultreturnVal; + end; + + + + +end; + + +end. diff --git a/src/screens/UScreenOptions.pas b/src/screens/UScreenOptions.pas index 236c05f69..0d8c98b9a 100644 --- a/src/screens/UScreenOptions.pas +++ b/src/screens/UScreenOptions.pas @@ -58,6 +58,7 @@ TScreenOptions = class(TMenu) ButtonNetworkIID, ButtonWebcamIID, ButtonJukeboxIID, + ButtonBeatRecordIID, ButtonExitIID: cardinal; MapIIDtoDescID: array of integer; @@ -266,6 +267,13 @@ function TScreenOptions.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; Pre ScreenPopupError.ShowPopup(Language.Translate('ERROR_NO_SONGS')); end; + if Interaction = ButtonBeatRecordIID then + begin + AudioPlayback.PlaySound(SoundLib.Start); + FadeTo(@ScreenOptionsBeatPlay); + + end; + if Interaction = ButtonExitIID then begin Ini.Save; @@ -324,6 +332,8 @@ constructor TScreenOptions.Create; AddButtonChecked(Theme.Options.ButtonWebcam, OPTIONS_DESC_INDEX_WEBCAM, ButtonWebcamIID); AddButtonChecked(Theme.Options.ButtonJukebox, OPTIONS_DESC_INDEX_JUKEBOX, ButtonJukeboxIID); + AddButtonChecked(Theme.Options.ButtonBeatPlaying, OPTIONS_DESC_INDEX_BEAT_PLAYING, ButtonBeatRecordIID); + AddButtonChecked(Theme.Options.ButtonExit, OPTIONS_DESC_INDEX_BACK, ButtonExitIID); Interaction := 0; diff --git a/src/screens/UScreenOptionsRecord.pas b/src/screens/UScreenOptionsRecord.pas index d8285e542..5bf9dcc78 100644 --- a/src/screens/UScreenOptionsRecord.pas +++ b/src/screens/UScreenOptionsRecord.pas @@ -68,6 +68,7 @@ TScreenOptionsRecord = class(TMenu) SelectChannelID: integer; SelectAssigneeID: integer; SelectThresholdID: integer; + SelectMicBoostID: integer; // interaction IDs ExitButtonIID: integer; @@ -347,7 +348,7 @@ constructor TScreenOptionsRecord.Create; Theme.OptionsRecord.SelectMicBoost.Y := Theme.OptionsRecord.SelectMicBoost.Y + SourceBarsTotalHeight + ChannelBarsTotalHeight; - AddSelectSlide(Theme.OptionsRecord.SelectMicBoost, Ini.MicBoost, IMicBoostTranslated); + SelectMicBoostID:=AddSelectSlide(Theme.OptionsRecord.SelectMicBoost, Ini.MicBoost, IMicBoostTranslated); end; @@ -432,6 +433,11 @@ procedure TScreenOptionsRecord.UpdateInputDevice; end; end; + // Make sure changes in mic boost level done anywhere else are picked + // up, particularly during the onShow procedure + UpdateSelectSlideOptions(Theme.OptionsRecord.SelectMicBoost, + SelectMicBoostID, IMicBoostTranslated, Ini.MicBoost); + StartPreview(); end; From f14ac8477363ea878cc4ebb8c12219e359a6c91f Mon Sep 17 00:00:00 2001 From: tbgitoo Date: Sat, 2 Oct 2021 23:32:14 +0200 Subject: [PATCH 5/6] Update compilation units --- src/ultrastardx.dpr | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ultrastardx.dpr b/src/ultrastardx.dpr index 240fa39ab..3c1929a94 100644 --- a/src/ultrastardx.dpr +++ b/src/ultrastardx.dpr @@ -247,6 +247,14 @@ uses UHelp in 'base\UHelp.pas', + + //------------------------------ + //Includes -Beat Playing + //------------------------------ + + UBeatNote in 'beatNote\UBeatNote.pas', + UBeatNoteTimer in 'beatNote\UBeatNoteTimer.pas', + //------------------------------ //Includes - Plugin Support //------------------------------ @@ -394,6 +402,11 @@ uses UScreenJukeboxPlaylist in 'screens\UScreenJukeboxPlaylist.pas', UAvatars in 'base\UAvatars.pas', + + UScreenOptionsBeatPlay in 'beatNote\UScreenOptionsBeatPlay.pas', + UScreenOptionsBeatPlayPeakAnalysis in 'beatNote\UScreenOptionsBeatPlayPeakAnalysis.pas', + + UScreenAbout in 'screens\UScreenAbout.pas', SysUtils; From 421b7e9c8f2ba8aa9475352a06f612a8182520f4 Mon Sep 17 00:00:00 2001 From: tbgitoo Date: Sat, 2 Oct 2021 23:44:02 +0200 Subject: [PATCH 6/6] Song loading, editing, saving --- src/base/UFiles.pas | 5 +++++ src/screens/UScreenEditSub.pas | 23 +++++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/base/UFiles.pas b/src/base/UFiles.pas index 55ea526e5..2757b800a 100644 --- a/src/base/UFiles.pas +++ b/src/base/UFiles.pas @@ -174,6 +174,11 @@ function SaveSong(const Song: TSong; const Tracks: array of TLines; const Name: SongFile.WriteLine('#P2:' + EncodeToken(Song.DuetNames[1])); end; + if Song.RapBeat then + begin + SongFile.WriteLine('#RAP:BEAT'); + end; + // write custom header tags WriteCustomTags; diff --git a/src/screens/UScreenEditSub.pas b/src/screens/UScreenEditSub.pas index 46de0d098..386ef7972 100644 --- a/src/screens/UScreenEditSub.pas +++ b/src/screens/UScreenEditSub.pas @@ -292,6 +292,7 @@ TScreenEditSub = class(TMenu) PlayOnlyButtonID: Integer; PlayWithNoteButtonID: Integer; PlayNoteButtonID: Integer; + RapBeatModeButtonID: Integer; // background image & video preview BackgroundImageId: Integer; Empty: array of UTF8String; //temporary variable to initialize slide - todo change @@ -1623,7 +1624,7 @@ function TScreenEditSub.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; Pre TextEditMode := true; end; - if Interaction = 24 then // UndoButtonId + if Interaction = 29 then // UndoButtonId begin CopyFromUndo; GoldenRec.KillAll; @@ -1631,17 +1632,17 @@ function TScreenEditSub.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; Pre ShowInteractiveBackground; end; - if Interaction = 25 then // PreviousSeqButtonID + if Interaction = 30 then // PreviousSeqButtonID begin PreviousSentence; end; - if Interaction = 26 then // NextSeqButtonID + if Interaction = 31 then // NextSeqButtonID begin NextSentence; end; - if Interaction = 27 then // FreestyleButtonID + if Interaction = 25 then // FreestyleButtonID begin CopyToUndo; if (Tracks[CurrentTrack].Lines[Tracks[CurrentTrack].CurrentLine].Notes[CurrentNote[CurrentTrack]].NoteType = ntFreestyle) then @@ -1664,7 +1665,7 @@ function TScreenEditSub.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; Pre Exit; end; - if Interaction = 28 then // GoldButtonID + if Interaction = 26 then // GoldButtonID begin CopyToUndo; if (Tracks[CurrentTrack].Lines[Tracks[CurrentTrack].CurrentLine].Notes[CurrentNote[CurrentTrack]].NoteType = ntGolden) then @@ -1683,7 +1684,7 @@ function TScreenEditSub.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; Pre Exit; end; - if Interaction = 29 then // PlayOnlyButtonID + if Interaction = 28 then // PlayOnlyButtonID begin // Play Sentence Click := true; @@ -1704,7 +1705,7 @@ function TScreenEditSub.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; Pre Text[TextInfo].Text := Language.Translate('EDIT_INFO_PLAY_SENTENCE'); end; - if Interaction = 30 then // PlayWithNoteButtonID + if Interaction = 27 then // PlayWithNoteButtonID begin Tracks[CurrentTrack].Lines[Tracks[CurrentTrack].CurrentLine].Notes[CurrentNote[CurrentTrack]].Color := 1; CurrentNote[CurrentTrack] := 0; @@ -1726,7 +1727,7 @@ function TScreenEditSub.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; Pre Text[TextInfo].Text := Language.Translate('EDIT_INFO_PLAY_SENTENCE'); end; - if Interaction = 31 then // PlayNoteButtonID + if Interaction = 24 then // PlayNoteButtonID begin Tracks[CurrentTrack].Lines[Tracks[CurrentTrack].CurrentLine].Notes[CurrentNote[CurrentTrack]].Color := 1; CurrentNote[CurrentTrack] := 0; @@ -1741,6 +1742,11 @@ function TScreenEditSub.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; Pre Text[TextInfo].Text := Language.Translate('EDIT_INFO_PLAY_SENTENCE'); end; + if Interaction=32 then + begin + CurrentSong.RapBeat := not CurrentSong.RapBeat; + end; + for LineIndex := 0 to Tracks[CurrentTrack].High do begin if Interaction = InteractiveLineId[LineIndex] then @@ -4400,6 +4406,7 @@ constructor TScreenEditSub.Create; PlayOnlyButtonID := AddButton(Theme.EditSub.PlayOnly); PlayWithNoteButtonID := AddButton(Theme.EditSub.PlayWithNote); PlayNoteButtonID := AddButton(Theme.EditSub.PlayNote); + RapBeatModeButtonID := AddButton(Theme.EditSub.RapBeatMode); // current line TextSentence := AddButton(Theme.EditSub.ButtonCurrentLine);