├── addons
├── config-ui.js
├── timeline
│ ├── style.css
│ ├── img
│ │ ├── 16
│ │ │ ├── new.png
│ │ │ ├── delete.png
│ │ │ ├── zoomin.png
│ │ │ ├── zoomout.png
│ │ │ ├── moveleft.png
│ │ │ └── moveright.png
│ │ ├── 24
│ │ │ ├── new.png
│ │ │ ├── delete.png
│ │ │ ├── zoomin.png
│ │ │ ├── zoomout.png
│ │ │ ├── moveleft.png
│ │ │ └── moveright.png
│ │ ├── 32
│ │ │ ├── new.png
│ │ │ ├── delete.png
│ │ │ ├── zoomin.png
│ │ │ ├── zoomout.png
│ │ │ ├── moveleft.png
│ │ │ └── moveright.png
│ │ ├── cluster_bg.png
│ │ ├── deleteEvent.png
│ │ └── themeswitcher
│ │ │ ├── buttonbg.png
│ │ │ ├── menuhoverbg.png
│ │ │ ├── icon_color_arrow.gif
│ │ │ ├── theme_90_blitzer.png
│ │ │ ├── theme_90_dot_luv.png
│ │ │ ├── theme_90_flick.png
│ │ │ ├── theme_90_le_frog.png
│ │ │ ├── theme_90_sunny.png
│ │ │ ├── theme_90_ui_dark.png
│ │ │ ├── theme_90_windoze.png
│ │ │ ├── theme_90_black_tie.png
│ │ │ ├── theme_90_cupertino.png
│ │ │ ├── theme_90_dark_hive.png
│ │ │ ├── theme_90_eggplant.png
│ │ │ ├── theme_90_humanity.png
│ │ │ ├── theme_90_overcast.png
│ │ │ ├── theme_90_ui_light.png
│ │ │ ├── theme_90_black_matte.png
│ │ │ ├── theme_90_excite_bike.png
│ │ │ ├── theme_90_hot_sneaks.png
│ │ │ ├── theme_90_mint_choco.png
│ │ │ ├── theme_90_smoothness.png
│ │ │ ├── theme_90_south_street.png
│ │ │ ├── theme_90_start_menu.png
│ │ │ ├── theme_90_swanky_purse.png
│ │ │ ├── theme_90_trontastic.png
│ │ │ └── theme_90_pepper_grinder.png
│ ├── timeline.html
│ ├── timeline.css
│ └── script.js
├── current_song
│ ├── rs_pick.png
│ ├── README.txt
│ ├── current_song.html
│ ├── style.css
│ └── script.js
├── Arcade_v1_LaS
│ ├── ARCADE_N.TTF
│ ├── README.txt
│ ├── Arcade.html
│ └── style.css
├── Arcade_v1_SA
│ ├── ARCADE_N.TTF
│ ├── README.txt
│ ├── style.css
│ └── Arcade.html
├── _deps
│ ├── images
│ │ ├── ui-icons_444444_256x240.png
│ │ ├── ui-icons_555555_256x240.png
│ │ ├── ui-icons_777620_256x240.png
│ │ ├── ui-icons_777777_256x240.png
│ │ ├── ui-icons_cc0000_256x240.png
│ │ └── ui-icons_ffffff_256x240.png
│ ├── sniffer-storage.js
│ ├── jquery.animateNumber.js
│ ├── sniffer-poller.js
│ └── playthrough-tracker.js
├── config.js
├── song_logger
│ ├── style.css
│ ├── README.txt
│ ├── script.js
│ └── song_logger.html
├── current_measure
│ ├── style.css
│ ├── current_measure.html
│ └── script.js
├── debug_addon
│ ├── script.js
│ └── debug_addon.html
├── vocals
│ ├── style.css
│ ├── rocksmith-style.css
│ ├── kick.css
│ ├── vocals.html
│ └── script.js
├── note_streaks
│ ├── README.txt
│ ├── note_streaks.html
│ ├── script.js
│ ├── style_orig.css
│ └── style.css
├── accuracy_chart
│ ├── accuracy_chart.html
│ └── script.js
├── current_song_v3.1_LaS
│ ├── README.txt
│ ├── style.css
│ └── current_song_v3.1.html
├── current_song_v2
│ ├── style.css
│ ├── current_song_v2.html
│ └── script.js
├── current_song_v4
│ ├── style.css
│ ├── current_song.html
│ └── script.js
├── README.txt
└── current_song_v3
│ ├── style.css
│ └── current_song_v3.html
├── RockSniffer
├── sniffy.ico
├── Addons
│ ├── Storage
│ │ ├── IAddonStorage.cs
│ │ ├── MemoryStorage.cs
│ │ └── SQLiteStorage.cs
│ └── AddonService.cs
├── Configuration
│ ├── OutputFile.cs
│ ├── FormatSettings.cs
│ ├── RPCSettings.cs
│ ├── AddonSettings.cs
│ ├── DebugSettings.cs
│ ├── OutputSettings.cs
│ └── Config.cs
├── RockSniffer.csproj
└── RPC
│ ├── AlbumArtResolver.cs
│ └── DiscordRPCHandler.cs
├── global.json
├── .gitmodules
├── README.md
├── LICENSE
├── .github
└── workflows
│ └── ci.yml
├── .gitattributes
├── RockSniffer.sln
└── .gitignore
/addons/config-ui.js:
--------------------------------------------------------------------------------
1 | // Assign default path for poller to inspect
2 | var defaultPath = "Lead";
3 |
--------------------------------------------------------------------------------
/addons/timeline/style.css:
--------------------------------------------------------------------------------
1 | /* This applies to all the text */
2 | body {
3 | font-family: arial;
4 | }
--------------------------------------------------------------------------------
/RockSniffer/sniffy.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/RockSniffer/sniffy.ico
--------------------------------------------------------------------------------
/addons/current_song/rs_pick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/current_song/rs_pick.png
--------------------------------------------------------------------------------
/addons/timeline/img/16/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/16/new.png
--------------------------------------------------------------------------------
/addons/timeline/img/24/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/24/new.png
--------------------------------------------------------------------------------
/addons/timeline/img/32/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/32/new.png
--------------------------------------------------------------------------------
/addons/Arcade_v1_LaS/ARCADE_N.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/Arcade_v1_LaS/ARCADE_N.TTF
--------------------------------------------------------------------------------
/addons/Arcade_v1_SA/ARCADE_N.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/Arcade_v1_SA/ARCADE_N.TTF
--------------------------------------------------------------------------------
/addons/timeline/img/16/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/16/delete.png
--------------------------------------------------------------------------------
/addons/timeline/img/16/zoomin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/16/zoomin.png
--------------------------------------------------------------------------------
/addons/timeline/img/16/zoomout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/16/zoomout.png
--------------------------------------------------------------------------------
/addons/timeline/img/24/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/24/delete.png
--------------------------------------------------------------------------------
/addons/timeline/img/24/zoomin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/24/zoomin.png
--------------------------------------------------------------------------------
/addons/timeline/img/24/zoomout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/24/zoomout.png
--------------------------------------------------------------------------------
/addons/timeline/img/32/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/32/delete.png
--------------------------------------------------------------------------------
/addons/timeline/img/32/zoomin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/32/zoomin.png
--------------------------------------------------------------------------------
/addons/timeline/img/32/zoomout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/32/zoomout.png
--------------------------------------------------------------------------------
/addons/timeline/img/cluster_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/cluster_bg.png
--------------------------------------------------------------------------------
/addons/timeline/img/16/moveleft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/16/moveleft.png
--------------------------------------------------------------------------------
/addons/timeline/img/16/moveright.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/16/moveright.png
--------------------------------------------------------------------------------
/addons/timeline/img/24/moveleft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/24/moveleft.png
--------------------------------------------------------------------------------
/addons/timeline/img/24/moveright.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/24/moveright.png
--------------------------------------------------------------------------------
/addons/timeline/img/32/moveleft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/32/moveleft.png
--------------------------------------------------------------------------------
/addons/timeline/img/32/moveright.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/32/moveright.png
--------------------------------------------------------------------------------
/addons/timeline/img/deleteEvent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/deleteEvent.png
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "6.0.0",
4 | "rollForward": "latestMajor",
5 | "allowPrerelease": false
6 | }
7 | }
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/buttonbg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/buttonbg.png
--------------------------------------------------------------------------------
/addons/_deps/images/ui-icons_444444_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/_deps/images/ui-icons_444444_256x240.png
--------------------------------------------------------------------------------
/addons/_deps/images/ui-icons_555555_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/_deps/images/ui-icons_555555_256x240.png
--------------------------------------------------------------------------------
/addons/_deps/images/ui-icons_777620_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/_deps/images/ui-icons_777620_256x240.png
--------------------------------------------------------------------------------
/addons/_deps/images/ui-icons_777777_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/_deps/images/ui-icons_777777_256x240.png
--------------------------------------------------------------------------------
/addons/_deps/images/ui-icons_cc0000_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/_deps/images/ui-icons_cc0000_256x240.png
--------------------------------------------------------------------------------
/addons/_deps/images/ui-icons_ffffff_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/_deps/images/ui-icons_ffffff_256x240.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/menuhoverbg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/menuhoverbg.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/icon_color_arrow.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/icon_color_arrow.gif
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_blitzer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_blitzer.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_dot_luv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_dot_luv.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_flick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_flick.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_le_frog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_le_frog.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_sunny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_sunny.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_ui_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_ui_dark.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_windoze.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_windoze.png
--------------------------------------------------------------------------------
/addons/config.js:
--------------------------------------------------------------------------------
1 | // THIS FILE IS AUTOMATICALLY GENERATED
2 |
3 | // Sniffer addon service connection details
4 | var ip = "127.0.0.1";
5 | var port = 9938;
6 |
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_black_tie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_black_tie.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_cupertino.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_cupertino.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_dark_hive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_dark_hive.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_eggplant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_eggplant.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_humanity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_humanity.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_overcast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_overcast.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_ui_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_ui_light.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_black_matte.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_black_matte.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_excite_bike.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_excite_bike.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_hot_sneaks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_hot_sneaks.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_mint_choco.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_mint_choco.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_smoothness.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_smoothness.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_south_street.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_south_street.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_start_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_start_menu.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_swanky_purse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_swanky_purse.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_trontastic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_trontastic.png
--------------------------------------------------------------------------------
/addons/timeline/img/themeswitcher/theme_90_pepper_grinder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tnt-coders/RockSniffer/master/addons/timeline/img/themeswitcher/theme_90_pepper_grinder.png
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "RockSnifferLib"]
2 | path = RockSnifferLib
3 | url = https://github.com/kokolihapihvi/RockSnifferLib.git
4 | [submodule "Rocksmith2014PsarcLib"]
5 | path = Rocksmith2014PsarcLib
6 | url = https://github.com/kokolihapihvi/Rocksmith2014PsarcLib.git
7 |
--------------------------------------------------------------------------------
/RockSniffer/Addons/Storage/IAddonStorage.cs:
--------------------------------------------------------------------------------
1 | namespace RockSniffer.Addons.Storage
2 | {
3 | public interface IAddonStorage
4 | {
5 | string GetValue(string addonid, string key);
6 | void SetValue(string addonid, string key, string value);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/addons/song_logger/style.css:
--------------------------------------------------------------------------------
1 | /* This applies to all the text */
2 | body {
3 | font-family: arial;
4 | }
5 |
6 | /* This is the div that contains all log lines */
7 | div.log {
8 |
9 | }
10 |
11 | /* This is a log line */
12 | div.log_line {
13 | display: inline-block;
14 | }
--------------------------------------------------------------------------------
/addons/current_measure/style.css:
--------------------------------------------------------------------------------
1 | /* This applies to all the text */
2 | body {
3 | font-family: arial;
4 | }
5 |
6 | /* This applies to the information text */
7 | h1 {
8 | font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
9 | font-size: 5vw;
10 | color: black;
11 | padding: 0;
12 | margin: 0;
13 | text-align: center;
14 | }
--------------------------------------------------------------------------------
/addons/song_logger/README.txt:
--------------------------------------------------------------------------------
1 | This addon keeps a log of songs as you play, useful if you want to keep track of what you've played
2 |
3 | Open song_logger.html in your browser and it will keep track of songs through the addon service
4 |
5 | NOTE: You will need to enable addons in config/addons.json
6 |
7 | Thanks <3 to:
8 | https://www.twitch.tv/wr4thtv for the inspiration
--------------------------------------------------------------------------------
/RockSniffer/Configuration/OutputFile.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RockSniffer.Configuration
4 | {
5 | [Serializable]
6 | public class OutputFile
7 | {
8 | public string filename;
9 | public string format;
10 |
11 | public OutputFile(string filename, string format)
12 | {
13 | this.filename = filename;
14 | this.format = format;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/addons/song_logger/script.js:
--------------------------------------------------------------------------------
1 | //Remember previous song
2 | var prevSongID = "";
3 |
4 | //Listen to onSongStarted
5 | var poller = new SnifferPoller({
6 | onSongStarted: function(song) {
7 | if(song.songID == prevSongID) {
8 | return;
9 | }
10 |
11 | var song = song.artistName + " - " + song.songName;
12 | $("div.log").append("
"+song+"
");
13 |
14 | prevSongID = song.songID;
15 | }
16 | });
--------------------------------------------------------------------------------
/RockSniffer/Configuration/FormatSettings.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Runtime.Serialization;
5 |
6 | namespace RockSniffer.Configuration
7 | {
8 | ///
9 | /// Class to hold the config settings
10 | ///
11 | [Serializable]
12 | public class FormatSettings
13 | {
14 | public string timeFormat = @"mm\:ss";
15 | public string percentageFormat = @"{0:f2}%";
16 | }
17 | }
--------------------------------------------------------------------------------
/RockSniffer/Configuration/RPCSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace RockSniffer.Configuration
8 | {
9 | [Serializable]
10 | public class RPCSettings
11 | {
12 | public bool enabled = false;
13 | public uint updatePeriodMs = 1000;
14 | public string client_id = "573253140682375193";
15 | public bool enableCoverArt = true;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/addons/debug_addon/script.js:
--------------------------------------------------------------------------------
1 | //How often to poll the addon service (in milliseconds)
2 | var pollrate = 900;
3 |
4 | //JQuerys document.onReady function
5 | //Gets called after the webpage is loaded
6 | $(function() {
7 | //Set a timer to refresh our data
8 | setInterval(refresh, pollrate);
9 | refresh();
10 | });
11 |
12 | function refresh() {
13 | //JSON query the addon service
14 | $.get("http://"+ip+":"+port, function(data) {
15 | $("div.data").html(""+JSON.stringify(data, null, 4)+"
2 |
3 |
4 |
5 |
6 | RockSniffer Debug Addon
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/RockSniffer/Configuration/DebugSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RockSniffer.Configuration
4 | {
5 | [Serializable]
6 | public class DebugSettings
7 | {
8 | public bool disableVersionCheck = false;
9 | public bool debugStateMachine = false;
10 | public bool debugSongDetails = false;
11 | public bool debugCache = false;
12 | public bool debugMemoryReadout = false;
13 | public bool debugSystemHandleQuery = false;
14 | public bool debugFileDetailQuery = false;
15 | public bool debugProcessingQueue = false;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/addons/note_streaks/note_streaks.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Note Streak Display
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/addons/song_logger/song_logger.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Song Logger
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/addons/current_measure/current_measure.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Current Measure Display
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/addons/current_song/README.txt:
--------------------------------------------------------------------------------
1 | This addon shows a popup with the song details and album cover
2 | This version is tailored for twitch.tv/wr4thtv
3 |
4 | Just add current_song.html into OBS as a browser source
5 | The display will fade in once a song starts, and will fade out when the song ends
6 |
7 | If you'd like to preview it, edit script.js and change preview to true at the top of the file
8 | Just remember to change it back to false before using!
9 | And; don't forget to refresh the browser source ;)
10 |
11 | NOTE: You will need to enable addons in config/addons.json
12 |
13 | Thanks <3 to:
14 | https://www.twitch.tv/wr4thtv for the inspiration
15 | https://www.twitch.tv/l9_elias for helping with the graphical side
--------------------------------------------------------------------------------
/addons/current_measure/script.js:
--------------------------------------------------------------------------------
1 | var poller = new SnifferPoller({
2 | interval: 500,
3 |
4 | onData: function(data) {
5 | var time = data.memoryReadout.songTimer;
6 | var arr = poller.getCurrentArrangement();
7 |
8 | if(arr == null) {
9 | $(".measure").text("-");
10 | return;
11 | }
12 |
13 | var measures = arr.data.Measures;
14 |
15 | for (var i = measures.length - 1; i >= 0; i--) {
16 | if(measures[i].Time <= time) {
17 | $(".measure").text(poller.getCurrentSection().name+" | "+measures[i].Number);
18 | break;
19 | }
20 | }
21 | },
22 | onSongStarted: function(data) {
23 | $(".measure").text("0");
24 | },
25 | onSongEnded: function(data) {
26 | $(".measure").text("-");
27 | }
28 | });
--------------------------------------------------------------------------------
/addons/vocals/rocksmith-style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap');
2 | :root {
3 | --rocksmith-blue: #1189cd;
4 | --darkest: #0b0e0f; /* dark black */
5 | --rocksmith-grey: #424242;
6 | }
7 |
8 | body {
9 | font-family: "Nunito Sans", sans-serif;
10 | font-optical-sizing: auto;
11 | font-weight: 800;
12 | font-style: normal;
13 | font-variation-settings: "wdth" 100, "YTLC" 500;
14 | font-size: 36px;
15 | padding: 4px;
16 | margin: 0;
17 | text-align: left;
18 | text-shadow: 2px 2px 2px black;
19 | }
20 |
21 | /* All text */
22 | div.vocal {
23 | color: var(--rocksmith-grey);
24 | }
25 |
26 | /* Active text */
27 | span.lyric {
28 | color: var(--rocksmith-blue);
29 | }
--------------------------------------------------------------------------------
/addons/accuracy_chart/accuracy_chart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | RockSniffer Accuracy Chart
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/addons/_deps/sniffer-storage.js:
--------------------------------------------------------------------------------
1 | class SnifferStorage {
2 | constructor(addonID, options = {}) {
3 | this.addonID = addonID;
4 |
5 | if(!this.addonID) {
6 | throw "Must define addon id";
7 | }
8 |
9 | var defaultOptions = {
10 | ip: "127.0.0.1",
11 | port: "9938"
12 | }
13 |
14 | //Set up options
15 | this.options = {}
16 | $.extend(this.options, defaultOptions, options);
17 | }
18 |
19 | getValue(key) {
20 | return $.ajax({
21 | method: "GET",
22 | dataType: "text",
23 | url: "http://"+this.options.ip+":"+this.options.port+"/storage/"+this.addonID+"/"+key,
24 | crossDomain: true
25 | });
26 | }
27 |
28 | setValue(key, value) {
29 | if(typeof(value) === "object") {
30 | value = JSON.stringify(value);
31 | }
32 |
33 | return $.ajax({
34 | method: "PUT",
35 | crossDomain: true,
36 | url: "http://"+this.options.ip+":"+this.options.port+"/storage/"+this.addonID+"/"+key,
37 | data: value
38 | });
39 | }
40 | }
--------------------------------------------------------------------------------
/addons/vocals/kick.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap');
2 | :root {
3 | --kick-green: #53fc18; /* Neon Green */
4 | --darkest: #0b0e0f; /* dark black: rgb: 11, 14, 15*/
5 | --darker: #191b1f; /* black */
6 | --dark: #24272c; /* light black */
7 | --gray: #474f54; /* gray */
8 | }
9 |
10 | body {
11 | font-family: "Nunito Sans", sans-serif;
12 | font-optical-sizing: auto;
13 | font-weight: 400;
14 | font-style: normal;
15 | font-variation-settings: "wdth" 100, "YTLC" 500;
16 | font-size: 36px;
17 | padding: 4px;
18 | margin: 0;
19 | text-align: left;
20 | }
21 |
22 | /* All text */
23 | div.vocal {
24 | padding-left: 16px;
25 | border-radius: 0 20px 20px 0;
26 | color: var(--gray);
27 | background-color: rgba(11, 14, 15, 0.92);
28 | }
29 |
30 | /* Active text */
31 | span.lyric {
32 | color: var(--kick-green);
33 | }
--------------------------------------------------------------------------------
/addons/current_song_v3.1_LaS/README.txt:
--------------------------------------------------------------------------------
1 | Thanks for choosing PoizenJam's Arcade UI! I hope you enjoy!
2 |
3 | NOTES
4 |
5 | ___________
6 | UI SIZE
7 | ___________
8 |
9 | The size of the UI can be adjusted in the file 'style.css'. Simply edit:
10 |
11 | div.popup {
12 | width: 420px;
13 | display: flex;
14 | overflow: hidden;
15 | background-color: rgba(0,0,0,1);
16 | color: white;
17 | padding: 0px 0px 0px 0px;
18 | }
19 |
20 | div.mainContainer {
21 | width: 420px;
22 | }
23 |
24 |
25 | and replace '420px' with whatever size you need the UI to be.
26 |
27 |
28 | ___________
29 | DISABLE ALBUM ART
30 | ___________
31 |
32 | If you would like to disable the Album art, replace line 24 in current_song_v3.1.html
33 |
34 |
35 |
36 | with
37 |
38 | -->
39 |
40 | To re-enable, simply delete the at the beginning and end of the
--------------------------------------------------------------------------------
/addons/vocals/vocals.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Current Vocals Display
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/addons/current_song/current_song.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Current Song Display
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 kokolihapihvi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/RockSniffer/Addons/Storage/MemoryStorage.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace RockSniffer.Addons.Storage
4 | {
5 | public class MemoryStorage : IAddonStorage
6 | {
7 | public Dictionary> storage = new Dictionary>();
8 |
9 | public string GetValue(string addonid, string key)
10 | {
11 | if (storage.ContainsKey(addonid))
12 | {
13 | if (storage[addonid].ContainsKey(key))
14 | {
15 | return storage[addonid][key];
16 | }
17 | }
18 |
19 | return null;
20 | }
21 |
22 | public void SetValue(string addonid, string key, string value)
23 | {
24 | if (!storage.ContainsKey(addonid))
25 | {
26 | storage.Add(addonid, new Dictionary());
27 | }
28 |
29 | if (!storage[addonid].ContainsKey(key))
30 | {
31 | storage[addonid].Add(key, value);
32 | }
33 |
34 | storage[addonid][key] = value;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/RockSniffer/Configuration/OutputSettings.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Runtime.Serialization;
5 |
6 | namespace RockSniffer.Configuration
7 | {
8 | [Serializable]
9 | public class OutputSettings
10 | {
11 | public OutputSettings()
12 | {
13 | //Convert to array on constructor
14 | ConvertToArray();
15 | }
16 |
17 | [JsonIgnore]
18 | public OutputFile[] output;
19 |
20 | //Have a dictionary for easier configuration via editing json
21 | public Dictionary outputs = new Dictionary() {
22 | { "song_details.txt", "%SONG_ARTIST% - %SONG_NAME%" },
23 | { "album_details.txt", "%SONG_ALBUM% (%ALBUM_YEAR%)" },
24 | { "song_timer.txt", "%SONG_TIMER%/%SONG_LENGTH%" },
25 | { "notes.txt", "%NOTES_HIT%/%TOTAL_NOTES%" },
26 | { "accuracy.txt", "%CURRENT_ACCURACY%" },
27 | { "streaks.txt", "%CURRENT_STREAK%/%HIGHEST_STREAK%" }
28 | };
29 |
30 | //Convert dictionary to array
31 | private void ConvertToArray()
32 | {
33 | List outputList = new List();
34 |
35 | //Create OutputFile instance for each output
36 | foreach (KeyValuePair op in outputs)
37 | {
38 | outputList.Add(new OutputFile(op.Key, op.Value));
39 | }
40 |
41 | //Turn list into array
42 | output = outputList.ToArray();
43 | }
44 |
45 | [OnDeserialized]
46 | internal void OnDeserialized(StreamingContext context)
47 | {
48 | ConvertToArray();
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/addons/Arcade_v1_LaS/README.txt:
--------------------------------------------------------------------------------
1 | Thanks for choosing PoizenJam's Arcade UI! I hope you enjoy!
2 |
3 | NOTES
4 |
5 | ___________
6 | UI SIZE
7 | ___________
8 |
9 | The size of the UI can be adjusted in the file 'style.css'. Simply edit:
10 |
11 | div.popup {
12 | width: 420px;
13 | display: flex;
14 | overflow: hidden;
15 | background-color: rgba(0,0,0,1);
16 | color: white;
17 | padding: 0px 0px 0px 0px;
18 | }
19 |
20 | div.mainContainer {
21 | width: 420px;
22 | }
23 |
24 |
25 | and replace '420px' with whatever size you need the UI to be.
26 |
27 |
28 | ___________
29 | FONT SIZES
30 | ___________
31 | The various font sizes can be edited in style.css. Search for font-size; the 'div' it belongs to (e.g. songName or artistName) should be self-explanatory
32 |
33 |
34 | ___________
35 |
36 | CUSTOM FONT
37 | ___________
38 | To use a custom font, place the font file in this directory, and edit the following lines in style.css:
39 |
40 | @font-face {
41 | font-family: PixelFont;
42 | src: url(ARCADE_N.ttf);
43 | }
44 |
45 | Replace 'ARCADE_N.tff' with your chosen font.
46 |
47 |
48 |
49 | ___________
50 | CUSTOM COLORS
51 | ___________
52 | This is a bit more advanced. The colors of the main UI elements can be edited via style.css. They are stored in various 'a' classes and 'div' classes as RGB values (#000000)
53 |
54 | ___________
55 | DISABLE ALBUM ART
56 | ___________
57 |
58 | If you would like to disable the Album art, replace line 23 in Arcade.html
59 |
60 |
61 |
62 | with
63 |
64 | -->
65 |
66 | To re-enable, simply delete the at the beginning and end of the
--------------------------------------------------------------------------------
/addons/Arcade_v1_SA/README.txt:
--------------------------------------------------------------------------------
1 | Thanks for choosing PoizenJam's Arcade UI! I hope you enjoy!
2 |
3 | NOTES
4 |
5 | ___________
6 | UI SIZE
7 | ___________
8 |
9 | The size of the UI can be adjusted in the file 'style.css'. Simply edit:
10 |
11 | div.popup {
12 | width: 420px;
13 | display: flex;
14 | overflow: hidden;
15 | background-color: rgba(0,0,0,1);
16 | color: white;
17 | padding: 0px 0px 0px 0px;
18 | }
19 |
20 | div.mainContainer {
21 | width: 420px;
22 | }
23 |
24 |
25 | and replace '420px' with whatever size you need the UI to be.
26 |
27 |
28 | ___________
29 | FONT SIZES
30 | ___________
31 | The various font sizes can be edited in style.css. Search for font-size; the 'div' it belongs to (e.g. songName or artistName) should be self-explanatory
32 |
33 |
34 | ___________
35 |
36 | CUSTOM FONT
37 | ___________
38 | To use a custom font, place the font file in this directory, and edit the following lines in style.css:
39 |
40 | @font-face {
41 | font-family: PixelFont;
42 | src: url(ARCADE_N.ttf);
43 | }
44 |
45 | Replace 'ARCADE_N.tff' with your chosen font.
46 |
47 |
48 |
49 | ___________
50 | CUSTOM COLORS
51 | ___________
52 | This is a bit more advanced. The colors of the main UI elements can be edited via style.css. They are stored in various 'a' classes and 'div' classes as RGB values (#000000)
53 |
54 | ___________
55 | DISABLE ALBUM ART
56 | ___________
57 |
58 | If you would like to disable the Album art, replace line 23 in Arcade.html
59 |
60 |
61 |
62 | with
63 |
64 | -->
65 |
66 | To re-enable, simply delete the at the beginning and end of the
--------------------------------------------------------------------------------
/addons/current_song_v2/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Open Sans', sans-serif;
3 | font-size: 20px;
4 | }
5 |
6 | div.popup {
7 | width: 300px;
8 | display: flex;
9 | overflow: hidden;
10 | background-color: rgba(0,0,0,0.7);
11 | color: white;
12 | padding: 2px 6px 2px 6px;
13 | }
14 |
15 | div.mainContainer {
16 | flex-grow: 1;
17 | }
18 |
19 | div.songTitle {
20 |
21 | }
22 |
23 | a {
24 | -webkit-text-stroke: 0.2px black;
25 | }
26 |
27 | a.songName {
28 | font-size: 26px;
29 | }
30 |
31 | a.artistName {
32 |
33 | }
34 |
35 | /* MODE 0 */
36 | img.albumArt {
37 | width: 100%;
38 | }
39 |
40 | div.timer {
41 | display: flex;
42 | flex-direction: column;
43 | }
44 |
45 | div.timerBar {
46 | background-color: black;
47 | height: 8px;
48 | width: 100%;
49 | border: 1px solid lightgrey;
50 | position: relative;
51 | }
52 |
53 | div.timerBarSection {
54 | height: 8px;
55 | position: absolute;
56 | background-color: red;
57 | top: 0px;
58 | opacity: 0.6;
59 | border-style: solid;
60 | border-color: black;
61 | border-width: 0 1px 0 1px;
62 | }
63 |
64 | div.timestamps {
65 | display: flex;
66 | justify-content: space-between;
67 | font-size: 16px;
68 | }
69 |
70 | div.timerBarInner {
71 | background-color: white;
72 | height: 100%;
73 | }
74 |
75 | /* MODE 1 */
76 | div.sectionContainer {
77 | position: relative;
78 | height: 10px;
79 | }
80 |
81 | div.section {
82 | height: 10px;
83 | position: absolute;
84 | top: 0px;
85 | border-style: solid;
86 | border-color: black;
87 | border-width: 0 1px 0 1px;
88 | }
89 |
90 | /* TRANSITIONS */
91 | .fade-expand-enter-active, .fade-expand-leave-active {
92 | transition: all 1s ease;
93 | }
94 | .fade-expand-enter, .fade-expand-leave-to {
95 | opacity: 0;
96 | max-height: 0;
97 | }
98 | .fade-expand-leave, .fade-expand-enter-to {
99 | max-height: 500px;
100 | }
101 |
102 | .fade-enter-active, .fade-leave-active {
103 | transition: opacity 1s ease;
104 | }
105 | .fade-enter, .fade-leave-to {
106 | opacity: 0;
107 | }
--------------------------------------------------------------------------------
/RockSniffer/RockSniffer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | Exe
5 | false
6 | publish\
7 | true
8 | Disk
9 | false
10 | Foreground
11 | 7
12 | Days
13 | false
14 | false
15 | true
16 | 0
17 | 1.0.0.%2a
18 | false
19 | true
20 | false
21 | default
22 | AnyCPU;x64
23 | enable
24 |
25 |
26 | sniffy.ico
27 |
28 |
29 | none
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/addons/current_song_v4/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Verdana', sans-serif;
3 | font-size: 20px;
4 | }
5 |
6 | div.popup {
7 | width: 400px;
8 | display: flex;
9 | overflow: hidden;
10 | background-color: #004d4d;
11 | color: white;
12 | padding: 2px 6px 2px 6px;
13 | box-shadow: 4px 4px 8px #000000;
14 | }
15 |
16 | div.mainContainer {
17 | flex-grow: 1;
18 | }
19 |
20 | div.songTitle {
21 |
22 | }
23 |
24 | a {
25 | -webkit-text-stroke: 0.2px black;
26 | }
27 |
28 | a.songName {
29 | font-size: 26px;
30 | }
31 |
32 | a.artistName {
33 |
34 | }
35 |
36 | /* MODE 0 */
37 | img.albumArt {
38 | width: 100%;
39 | }
40 |
41 | div.timer {
42 | display: flex;
43 | flex-direction: column;
44 | }
45 |
46 | div.timerBar {
47 | background-color: black;
48 | height: 8px;
49 | width: 100%;
50 | border: 1px solid #006666;
51 | position: relative;
52 | }
53 |
54 | div.timerBarSection {
55 | height: 8px;
56 | position: absolute;
57 | background-color: #009999;
58 | top: 0px;
59 | opacity: 0.6;
60 | border-style: solid;
61 | border-color: black;
62 | border-width: 0;
63 | }
64 |
65 | div.timestamps {
66 | display: flex;
67 | justify-content: space-between;
68 | font-size: 16px;
69 | }
70 |
71 | div.timerBarInner {
72 | background-color: #009999;
73 | height: 100%;
74 | }
75 |
76 | /* MODE 1 */
77 | div.sectionContainer {
78 | position: relative;
79 | height: 10px;
80 | }
81 |
82 | div.section {
83 | height: 10px;
84 | position: absolute;
85 | top: 0px;
86 | border-style: solid;
87 | border-color: black;
88 | border-width: 0 1px 0 1px;
89 | }
90 |
91 | /* TRANSITIONS */
92 | .fade-expand-enter-active, .fade-expand-leave-active {
93 | transition: all 1s ease;
94 | }
95 | .fade-expand-enter, .fade-expand-leave-to {
96 | opacity: 0;
97 | max-height: 0;
98 | }
99 | .fade-expand-leave, .fade-expand-enter-to {
100 | max-height: 400px;
101 | }
102 |
103 | .fade-enter-active, .fade-leave-active {
104 | transition: opacity 1s ease;
105 | }
106 | .fade-enter, .fade-leave-to {
107 | opacity: 0;
108 | }
--------------------------------------------------------------------------------
/addons/note_streaks/script.js:
--------------------------------------------------------------------------------
1 | //Set this to true for previewing
2 | var preview = false;
3 |
4 | //How often to poll the addon service (in milliseconds)
5 | var pollrate = 900;
6 |
7 | //JQuerys document.onReady function
8 | //Gets called after the webpage is loaded
9 | $(function() {
10 | //Set a timer to refresh our data
11 | setInterval(refresh, pollrate);
12 | });
13 |
14 | //Remember popup visibility
15 | var visible = false;
16 |
17 | //Remember previous streak
18 | var prevStreak = 0;
19 |
20 | function refresh() {
21 | if(preview) {
22 | showPopup(Math.round(Math.random()*200));
23 | return;
24 | }
25 |
26 | //JSON query the addon service
27 | $.getJSON("http://"+ip+":"+port, function(data) {
28 | //If data was successfully gotten
29 | if(data.success) {
30 | //Get memory readout
31 | var ro = data.memoryReadout.noteData;
32 |
33 | //Get current note streak from readout
34 | var streak = ro.CurrentHitStreak;
35 |
36 | //If the previous streak is larger than current streak, the streak must have broken
37 | if(prevStreak > streak) {
38 | prevStreak = 0;
39 | }
40 |
41 | //Check all values between previous streak and current streak
42 | for (var i = prevStreak; i < streak; i++) {
43 | //If we hit 50, show popup
44 | if(i == 50) {
45 | showPopup(50);
46 | break;
47 | }
48 |
49 | //If we hit a multiple of 100, show popup
50 | if(i % 100 == 0) {
51 | showPopup(i);
52 | break;
53 | }
54 | }
55 |
56 | //Remember the streak
57 | prevStreak = streak;
58 | }
59 | }, "json");
60 | }
61 |
62 | //Shows the popup if it is hidden
63 | function showPopup(streak) {
64 | if(visible) return;
65 |
66 | //Ignore 0 note streaks
67 | if(streak == 0) return;
68 |
69 | //Set the text and make it visible
70 | $("h1.streak").text(streak+" NOTE STREAK!").css("display","block").css("animation-name","popup-animation"+(streak > 100 ? "-rainbow" : ""));
71 |
72 | //Allow re-showing after 3.5 seconds (animation duration + 500ms)
73 | setTimeout(function() {
74 | visible = false;
75 | $("h1.streak").css("display","none");
76 | }, 3500);
77 |
78 | //Remember visibility state
79 | visible = true;
80 | }
--------------------------------------------------------------------------------
/addons/note_streaks/style_orig.css:
--------------------------------------------------------------------------------
1 | @keyframes popup-animation {
2 | /*start of animation*/
3 | 0% {
4 | opacity: 0;
5 | font-size: 100vw;
6 | transform: rotate(0deg);
7 | }
8 |
9 | /*End of entering animation and start of first bounce*/
10 | 10% {
11 | opacity: 1;
12 | font-size: 3.5vw;
13 | transform: rotate(0deg);
14 | }
15 |
16 | /*End of first bounce*/
17 | 25% {
18 | font-size: 7vw;
19 | transform: rotate(5deg);
20 | }
21 |
22 | /*Start of second bounce*/
23 | 35% {
24 | font-size: 3.5vw;
25 | transform: rotate(0deg);
26 | }
27 |
28 | /*End of second bounce*/
29 | 45% {
30 | font-size: 6vw;
31 | transform: rotate(-4deg);
32 | }
33 |
34 | /*Start of third bounce*/
35 | 50% {
36 | font-size: 3.5vw;
37 | transform: rotate(0deg);
38 | }
39 |
40 | /*End of third bounce*/
41 | 60% {
42 | font-size: 5vw;
43 | transform: rotate(2deg);
44 | }
45 |
46 | /*Start of fourth bounce*/
47 | 65% {
48 | font-size: 3.5vw;
49 | transform: rotate(0deg);
50 | }
51 |
52 | /*End of fourth bounce*/
53 | 75% {
54 | font-size: 4vw;
55 | transform: rotate(-1deg);
56 | }
57 |
58 | /*Start of exit animation*/
59 | 90% {
60 | opacity: 1;
61 | font-size: 3.5vw;
62 | transform: rotate(0deg) translate(0,0);
63 | }
64 |
65 | /*Final state and end of exit animation*/
66 | 100% {
67 | opacity: 0;
68 | transform: translateY(-100px);
69 | }
70 | }
71 |
72 | /* This applies to all the text */
73 | body {
74 | font-family: arial;
75 | }
76 |
77 | /* This is the popup container, hide it by default */
78 | div.popup {
79 | /*display: none;*/
80 | text-align: center;
81 | }
82 |
83 | /* This applies to the information text */
84 | h1 {
85 | font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
86 | font-size: 4.5vw;
87 | color: white;
88 | line-height: 4.3vw;
89 | -webkit-text-stroke: 0.1vw black;
90 | }
91 |
92 | /* This applies to the header title text */
93 | h1.streak {
94 | animation-name: popup-animation;
95 | animation-duration: 3s;
96 | animation-timing-function: ease-in-out;
97 | animation-fill-mode: forwards;
98 | font-weight: bold;
99 | font-size: 3.5vw;
100 | color: white;
101 | padding-top: 22vw;
102 | display: none;
103 | }
--------------------------------------------------------------------------------
/addons/README.txt:
--------------------------------------------------------------------------------
1 | This directory includes addons that come with RockSniffer
2 |
3 | If you'd like to make your own, feel free!
4 |
5 | Here is how to query the addon service from your addon
6 | Note that this query is written with JQuery, a library found in _deps
7 | The _deps directory contains some common javascript libraries
8 |
9 | Also note that the addon service may be disabled by default, see config/addons.json
10 |
11 | //This will query data from the addon service
12 | $.getJSON("http://127.0.0.1:9938", function(data) {
13 | //When the query has completed, this function will run
14 | //Included in the data object you will find these fields:
15 |
16 | //data.success (boolean)
17 | //Was the query successful on the addon service side
18 |
19 | //data.albumCoverBase64 (string)
20 | //The album cover art in base64 encoding, see current_song addon for an example
21 |
22 | //data.memoryReadout (object)
23 | //The current memory readout data object
24 |
25 | //data.songDetails (object)
26 | //The current song details object
27 |
28 | //
29 | //The memory readout object
30 | //
31 |
32 | //memoryReadout.songTimer (float)
33 | //The song timer in seconds
34 |
35 | //memoryReadout.songID (string)
36 | //The current songs ID
37 |
38 | //memoryReadout.totalNotesHit (int)
39 | //The number of notes hit
40 |
41 | //memoryReadout.currentHitStreak (int)
42 | //Current hit streak
43 |
44 | //memoryReadout.unknown (int)
45 | //No idea what this number represents
46 |
47 | //memoryReadout.highestHitStreak (int)
48 | //The highest the hit streak has reached
49 |
50 | //memoryReadout.totalNotesMissed (int)
51 | //The number of notes missed in total
52 |
53 | //memoryReadout.currentMissStreak (int)
54 | //The number of notes missed in a row
55 |
56 | //
57 | //The song details object
58 | //
59 |
60 | //songDetails.songID (string)
61 | //The song ID of the current song
62 |
63 | //songDetails.songName (string)
64 | //The name of the current song
65 |
66 | //songDetails.artistName (string)
67 | //The artist name of the current song
68 |
69 | //songDetails.albumName (string)
70 | //The album name the song is from
71 |
72 | //songDetails.songLength (float)
73 | //The length of the song, in seconds
74 |
75 | //songDetails.albumYear (int)
76 | //The year the album was released
77 |
78 | //songDetails.numArrangements (int)
79 | //The number of arrangements the song has
80 | });
81 |
--------------------------------------------------------------------------------
/addons/timeline/timeline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer OBS Sequencer
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
48 |
49 | {{status}}
50 |
51 | Current Song:
52 | {{addonData.songDetails.songID}}: {{addonData.songDetails.artistName}} - {{addonData.songDetails.songName}}
53 |
54 |
55 |
No current song
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - master
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | with:
12 | submodules: 'recursive'
13 | - name: Setup .NET
14 | uses: actions/setup-dotnet@v4
15 | with:
16 | dotnet-version: 8.0.x
17 | - name: Restore dependencies
18 | run: dotnet restore
19 | - name: Build
20 | id: build
21 | run: |
22 | VERSION=$(sed -nr 's/^.*const string version.*"(.*)".*$/\1/pi' ./RockSniffer/Program.cs)
23 | echo "Sniffer version: $VERSION"
24 | echo "SNIFFER_VERSION=$VERSION" >> "$GITHUB_OUTPUT"
25 | dotnet build --configuration Release --no-restore
26 | - name: Publish
27 | env:
28 | SNIFFER_VERSION: ${{ steps.build.outputs.SNIFFER_VERSION }}
29 | run: |
30 | dotnet publish RockSniffer -c Release -o ./publish --runtime win-x64 --framework=net8.0 --self-contained false -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
31 | cp -r ./addons ./publish/addons
32 | (cd ./publish && zip -r "$OLDPWD/RockSniffer $SNIFFER_VERSION.zip" .)
33 | echo "$SNIFFER_VERSION" > VERSION
34 | - uses: actions/upload-artifact@v4
35 | with:
36 | name: RockSniffer-release
37 | path: |
38 | VERSION
39 | ./RockSniffer ${{ steps.build.outputs.SNIFFER_VERSION }}.zip
40 | release:
41 | runs-on: ubuntu-latest
42 | needs:
43 | - build
44 | permissions:
45 | contents: write
46 | steps:
47 | - uses: actions/download-artifact@v4
48 | with:
49 | name: RockSniffer-release
50 | - name: Get version
51 | id: get-version
52 | run: |
53 | echo "SNIFFER_VERSION=$(cat VERSION)" >> "$GITHUB_OUTPUT"
54 | - uses: ncipollo/release-action@v1
55 | with:
56 | name: v${{ steps.get-version.outputs.SNIFFER_VERSION }}
57 | tag: v${{ steps.get-version.outputs.SNIFFER_VERSION }}
58 | allowUpdates: true
59 | updateOnlyUnreleased: true
60 | commit: ${{ github.sha }}
61 | prerelease: true
62 | draft: true
63 | generateReleaseNotes: true
64 | omitBodyDuringUpdate: true
65 | omitDraftDuringUpdate: true
66 | removeArtifacts: true
67 | replacesArtifacts: true
68 | artifacts: "*.zip"
69 |
--------------------------------------------------------------------------------
/addons/current_song/style.css:
--------------------------------------------------------------------------------
1 | /* This applies to all the text */
2 | body {
3 | font-family: arial;
4 | }
5 |
6 | /* This is the popup container, hide it by default */
7 | div.popup {
8 | display: none;
9 | }
10 |
11 | /* This is the header of the popup, the section containing the text "CURRENT SONG" */
12 | div.header {
13 | font-family: Arial;
14 | width: 100%;
15 | height: 5vw;
16 | background-color: rgb(40, 56, 72);
17 | border-radius: 8px 8px 0 0;
18 | border-bottom: 0.5vw solid black;
19 | display: flex;
20 | align-items: center;
21 | }
22 |
23 | /* This is the popup block that contains all the data */
24 | div.content {
25 | overflow: hidden;
26 | padding-left: 2vw;
27 | padding-right: 2vw;
28 | background-color: rgba(85, 98, 111, 0.84);
29 | border-radius: 0 0 7px 7px;
30 | }
31 |
32 | div.song_details {
33 | width: 74%;
34 | }
35 |
36 | /* This applies to the information text */
37 | h1 {
38 | font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
39 | font-size: 4.5vw;
40 | color: white;
41 | line-height: 4.3vw;
42 | }
43 |
44 | /* Classes for outlined stroke text */
45 | .stroke {
46 | position: relative;
47 | z-index: 0;
48 | }
49 |
50 | .stroke:before {
51 | position: absolute;
52 | content: attr(data-stroke);
53 | -webkit-text-stroke: 0.1em #000000;
54 | z-index: -1;
55 | }
56 |
57 | /* This applies to the accuracy percentage in the title */
58 | h1.accuracy_percentage {
59 | font-size: 4.5vw !important;
60 | padding-left: 1vw;
61 | }
62 |
63 | /* This applies to the header title text */
64 | h1.title {
65 | display: inline-block;
66 | font-size: 3.5vw;
67 | color: white;
68 | }
69 |
70 | /* This applies to the rocksmith guitarpick in the top left corner of the popup */
71 | img.pick {
72 | height: 70%;
73 | float: left;
74 | padding-right: 1.5vw;
75 | margin-left: 7px;
76 | }
77 |
78 | /* This applies to the album cover on the popup */
79 | img.album_cover {
80 | width: 23%;
81 | float: right;
82 | margin: 2vw 0 1vw 0;
83 | border-style: solid;
84 | border-color: black;
85 | border-width: 0.5vw;
86 | }
87 |
88 | /* Classes for the progress bar */
89 | div.progress_bar {
90 | background-color: rgb(40, 56, 72);
91 | height: 4vw;
92 | font-size: 4vw;
93 | margin: 0 0 1vw 0;
94 | clear: both;
95 | text-align: center;
96 | }
97 |
98 | div.progress_bar_inner {
99 | background-color: rgba(95, 108, 121, 0.3);
100 | height: 100%;
101 | width: 0;
102 | }
103 |
104 | p.progress_bar_text {
105 | padding: 0;
106 | margin: 0;
107 | height: 0;
108 | overflow: visible;
109 | color: white;
110 | }
--------------------------------------------------------------------------------
/addons/current_song_v4/current_song.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Current Song Display
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/RockSniffer/Addons/AddonService.cs:
--------------------------------------------------------------------------------
1 | using RockSniffer.Addons.Storage;
2 | using RockSniffer.Configuration;
3 | using RockSnifferLib.Sniffing;
4 | using System;
5 | using System.IO;
6 | using System.Net;
7 |
8 | namespace RockSniffer.Addons
9 | {
10 | internal class AddonService
11 | {
12 | internal static string AddonsPath { get; private set; }
13 |
14 | private AddonServiceListener listener;
15 |
16 | public AddonService(AddonSettings settings, IAddonStorage storage)
17 | {
18 | if (!IPAddress.TryParse(settings.ipAddress, out IPAddress ip))
19 | {
20 | throw new Exception($"IP Address '{settings.ipAddress}' is not valid");
21 | }
22 |
23 | Console.WriteLine("Starting AddonService listener on {0}:{1}", ip.ToString(), settings.port);
24 |
25 | // If addons folder was found, auto generate config file
26 | if (FindAddons())
27 | {
28 | GenerateConfig(settings);
29 | }
30 | else
31 | {
32 | Console.WriteLine("Addons folder not found, skipped config.js generation");
33 | }
34 |
35 | listener = new AddonServiceListener(ip, settings, storage);
36 | }
37 |
38 | ///
39 | /// Try to find the addons folder
40 | ///
41 | private bool FindAddons()
42 | {
43 | var path = "./addons";
44 |
45 | if (Directory.Exists(path))
46 | {
47 | AddonsPath = path;
48 | return true;
49 | }
50 | #if DEBUG // Find addons from the git root, if running in debug mode for development
51 | path = "../../../../addons";
52 | if (Directory.Exists(path))
53 | {
54 | AddonsPath = path;
55 | return true;
56 | }
57 | #endif
58 |
59 | return false;
60 | }
61 |
62 | private void GenerateConfig(AddonSettings settings)
63 | {
64 | // Generate a config js file that addons can use, to make configuring 2pc setup addons less painful
65 | string pattern = $@"// THIS FILE IS AUTOMATICALLY GENERATED
66 |
67 | // Sniffer addon service connection details
68 | var ip = ""{settings.ipAddress}"";
69 | var port = {settings.port};
70 | ";
71 |
72 | var configPath = Path.Combine(AddonsPath, "config.js");
73 |
74 | File.WriteAllText(configPath, pattern);
75 | }
76 |
77 | public void SetSniffer(Sniffer sniffer)
78 | {
79 | sniffer.OnSongChanged += listener.OnCurrentSongChanged;
80 | sniffer.OnMemoryReadout += listener.OnMemoryReadout;
81 | sniffer.OnStateChanged += listener.OnStateChanged;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/RockSniffer/Addons/Storage/SQLiteStorage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data.SQLite;
3 | using System.IO;
4 |
5 | namespace RockSniffer.Addons.Storage
6 | {
7 | public class SQLiteStorage : IAddonStorage
8 | {
9 | private SQLiteConnection Connection { get; set; }
10 |
11 | public SQLiteStorage()
12 | {
13 | if (!File.Exists("addonstorage.sqlite"))
14 | {
15 | SQLiteConnection.CreateFile("addonstorage.sqlite");
16 | }
17 |
18 | Environment.SetEnvironmentVariable("SQLite_ConfigureDirectory", ".");
19 | Connection = new SQLiteConnection("Data Source=addonstorage.sqlite;");
20 | Connection.Open();
21 | }
22 |
23 | private void CreateTable(string addonkey)
24 | {
25 | var q = $@"
26 | CREATE TABLE IF NOT EXISTS `{addonkey}` (
27 | `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
28 | `key` TEXT NOT NULL,
29 | `value` TEXT
30 | );";
31 |
32 | using (var cmd = Connection.CreateCommand())
33 | {
34 | cmd.CommandText = q;
35 | cmd.ExecuteNonQuery();
36 | }
37 | }
38 |
39 | public string GetValue(string addonid, string key)
40 | {
41 | //Make sure the table exists
42 | CreateTable(addonid);
43 |
44 | using (var cmd = Connection.CreateCommand())
45 | {
46 | cmd.CommandText = $"SELECT value FROM {addonid} WHERE key=@key";
47 |
48 | cmd.Parameters.AddWithValue("@key", key);
49 |
50 | return (string)cmd.ExecuteScalar();
51 | }
52 | }
53 |
54 | public void SetValue(string addonid, string key, string value)
55 | {
56 | Console.WriteLine($"Storing {addonid}/{key} ({value.Length} bytes)");
57 |
58 | //Make sure the table exists
59 | CreateTable(addonid);
60 |
61 | //Attempt to update the table
62 | using (var cmd = Connection.CreateCommand())
63 | {
64 | cmd.CommandText = $"UPDATE {addonid} SET value=@value WHERE key=@key";
65 |
66 | cmd.Parameters.AddWithValue("@value", value);
67 | cmd.Parameters.AddWithValue("@key", key);
68 |
69 | //If update changed more than 0 rows, return
70 | if (cmd.ExecuteNonQuery() > 0)
71 | {
72 | return;
73 | }
74 | }
75 |
76 | using (var cmd = Connection.CreateCommand())
77 | {
78 | cmd.CommandText = $@"INSERT INTO {addonid}(key, value) VALUES(@key, @value)";
79 |
80 | cmd.Parameters.AddWithValue("@value", value);
81 | cmd.Parameters.AddWithValue("@key", key);
82 |
83 | cmd.ExecuteNonQuery();
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/addons/current_song_v2/current_song_v2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Current Song Display
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/RockSniffer.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30114.105
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockSniffer", "RockSniffer\RockSniffer.csproj", "{8B784AB7-8D24-4AFC-AA34-4A7299C6A650}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockSnifferLib", "RockSnifferLib\RockSnifferLib.csproj", "{5C132036-7413-432E-B03E-13A1015E3FB8}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rocksmith2014PsarcLib", "Rocksmith2014PsarcLib\Rocksmith2014PsarcLib.csproj", "{8EA027ED-0A76-4913-9486-B2BAB27D9754}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Debug|x64 = Debug|x64
16 | Release|Any CPU = Release|Any CPU
17 | Release|x64 = Release|x64
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {8B784AB7-8D24-4AFC-AA34-4A7299C6A650}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {8B784AB7-8D24-4AFC-AA34-4A7299C6A650}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {8B784AB7-8D24-4AFC-AA34-4A7299C6A650}.Debug|x64.ActiveCfg = Debug|x64
23 | {8B784AB7-8D24-4AFC-AA34-4A7299C6A650}.Debug|x64.Build.0 = Debug|x64
24 | {8B784AB7-8D24-4AFC-AA34-4A7299C6A650}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {8B784AB7-8D24-4AFC-AA34-4A7299C6A650}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {8B784AB7-8D24-4AFC-AA34-4A7299C6A650}.Release|x64.ActiveCfg = Release|x64
27 | {8B784AB7-8D24-4AFC-AA34-4A7299C6A650}.Release|x64.Build.0 = Release|x64
28 | {5C132036-7413-432E-B03E-13A1015E3FB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {5C132036-7413-432E-B03E-13A1015E3FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {5C132036-7413-432E-B03E-13A1015E3FB8}.Debug|x64.ActiveCfg = Debug|x64
31 | {5C132036-7413-432E-B03E-13A1015E3FB8}.Debug|x64.Build.0 = Debug|x64
32 | {5C132036-7413-432E-B03E-13A1015E3FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {5C132036-7413-432E-B03E-13A1015E3FB8}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {5C132036-7413-432E-B03E-13A1015E3FB8}.Release|x64.ActiveCfg = Release|x64
35 | {5C132036-7413-432E-B03E-13A1015E3FB8}.Release|x64.Build.0 = Release|x64
36 | {8EA027ED-0A76-4913-9486-B2BAB27D9754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {8EA027ED-0A76-4913-9486-B2BAB27D9754}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {8EA027ED-0A76-4913-9486-B2BAB27D9754}.Debug|x64.ActiveCfg = Debug|x64
39 | {8EA027ED-0A76-4913-9486-B2BAB27D9754}.Debug|x64.Build.0 = Debug|x64
40 | {8EA027ED-0A76-4913-9486-B2BAB27D9754}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {8EA027ED-0A76-4913-9486-B2BAB27D9754}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {8EA027ED-0A76-4913-9486-B2BAB27D9754}.Release|x64.ActiveCfg = Release|x64
43 | {8EA027ED-0A76-4913-9486-B2BAB27D9754}.Release|x64.Build.0 = Release|x64
44 | EndGlobalSection
45 | GlobalSection(SolutionProperties) = preSolution
46 | HideSolutionNode = FALSE
47 | EndGlobalSection
48 | GlobalSection(ExtensibilityGlobals) = postSolution
49 | SolutionGuid = {807E9788-C924-42A6-BAC9-C94DD6EC1FC0}
50 | EndGlobalSection
51 | EndGlobal
52 |
--------------------------------------------------------------------------------
/RockSniffer/Configuration/Config.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using RockSnifferLib.Configuration;
3 | using System;
4 | using System.IO;
5 |
6 | namespace RockSniffer.Configuration
7 | {
8 | public class Config
9 | {
10 | private static readonly string cfiledir = "." + Path.DirectorySeparatorChar + "config" + Path.DirectorySeparatorChar;
11 | private const string addonFile = "addons.json";
12 | private const string snifferFile = "sniffer.json";
13 | private const string rpcFile = "rpc.json";
14 | private const string formatFile = "format.json";
15 | private const string debugFile = "debug.json";
16 | private const string outputFile = "output.json";
17 |
18 | public AddonSettings addonSettings;
19 | public SnifferSettings snifferSettings;
20 | public RPCSettings rpcSettings;
21 | public FormatSettings formatSettings;
22 | public DebugSettings debugSettings;
23 | public OutputSettings outputSettings;
24 |
25 | ///
26 | /// Load JSON files from disc or initialize default values and write all configuration files to disc
27 | ///
28 | internal void Load()
29 | {
30 | //Create the config directory if it doesn't exist
31 | Directory.CreateDirectory(cfiledir);
32 |
33 | //Load settings files
34 | addonSettings = LoadFile(addonFile);
35 | snifferSettings = LoadFile(snifferFile);
36 | rpcSettings = LoadFile(rpcFile);
37 | formatSettings = LoadFile(formatFile);
38 | debugSettings = LoadFile(debugFile);
39 | outputSettings = LoadFile(outputFile);
40 |
41 | Console.WriteLine("Configuration loaded");
42 |
43 | //Save the resulting JSON
44 | Save();
45 | }
46 |
47 | ///
48 | /// Load a configuration file
49 | ///
50 | ///
51 | ///
52 | ///
53 | private T LoadFile(string file) where T : new()
54 | {
55 | if (File.Exists(cfiledir + file))
56 | {
57 | string str = File.ReadAllText(cfiledir + file);
58 | return JsonConvert.DeserializeObject(str);
59 | }
60 |
61 | return new T();
62 | }
63 |
64 | ///
65 | /// Save all settings objects onto disc as JSON
66 | ///
67 | private void Save()
68 | {
69 | File.WriteAllText(cfiledir + addonFile, JsonConvert.SerializeObject(addonSettings, Formatting.Indented));
70 | File.WriteAllText(cfiledir + snifferFile, JsonConvert.SerializeObject(snifferSettings, Formatting.Indented));
71 | File.WriteAllText(cfiledir + rpcFile, JsonConvert.SerializeObject(rpcSettings, Formatting.Indented));
72 | File.WriteAllText(cfiledir + formatFile, JsonConvert.SerializeObject(formatSettings, Formatting.Indented));
73 | File.WriteAllText(cfiledir + debugFile, JsonConvert.SerializeObject(debugSettings, Formatting.Indented));
74 | File.WriteAllText(cfiledir + outputFile, JsonConvert.SerializeObject(outputSettings, Formatting.Indented));
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/addons/current_song_v3/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Lucida Console", Courier, monospace;
3 | font-size: 14px;
4 | }
5 |
6 | div.popup {
7 | width: 420px;
8 | display: flex;
9 | overflow: hidden;
10 | background-color: rgba(0,0,0,1);
11 | color: white;
12 | padding: 0px 0px 0px 0px;
13 | }
14 |
15 | div.mainContainer {
16 | width: 420px;
17 | }
18 |
19 | div.songInfo {
20 | white-space: nowrap;
21 | overflow: hidden;
22 | text-overflow: ellipsis;
23 | margin: 0px 5px 0px 5px;
24 | line-height: 20px;
25 |
26 | }
27 |
28 |
29 | div.songMarquee {
30 | transition: transform 2s ease-in-out;
31 | transform: translateX(0px);
32 | width: 100%;
33 | margin: 0px 0px 0px 0px;
34 | }
35 |
36 | a.songName {
37 | color: white;
38 | font-size: 18px;
39 | white-space: nowrap;
40 | }
41 |
42 | a.songDash {
43 | color: white;
44 | font-size: 18px;
45 | white-space: nowrap;
46 | }
47 |
48 | a.artistName {
49 | color: white;
50 | font-size: 18px;
51 | white-space: nowrap;
52 | }
53 |
54 | a.tuningName {
55 |
56 | }
57 |
58 | /* MODE 0 */
59 | img.albumArt {
60 | width: 100%;
61 | }
62 |
63 | div.stats{
64 | display: flex;
65 | width=100%;
66 | }
67 |
68 | div.statsLeft{
69 | display: flex;
70 | width: 50%;
71 | justify-content: space-between;
72 | margin: 0 5px 0 0;
73 | }
74 |
75 | div.statsRight{
76 | display: flex;
77 | width: 50%;
78 | justify-content: space-between;
79 | margin: 0 0 0 5px;
80 | }
81 |
82 | div.statsNAME{
83 | width: 25%;
84 | }
85 |
86 | div.statsNUM{
87 | font-size: 14px;
88 | width: 75%;
89 | text-align: right;
90 | }
91 | div.timer {
92 | margin: 2px 0 0 0;
93 | font-smooth: never;
94 | -webkit-font-smoothing : none;
95 | }
96 |
97 | div.timerBar {
98 | background-color: black;
99 | height: 13px;
100 | width: 100%;
101 | position: relative;
102 | border-width: 0 0 0 0;
103 | }
104 |
105 | div.timerBarPhrase{
106 | position: absolute;
107 | background-color: magenta;
108 | bottom: 1px;
109 | opacity: 1;
110 | border-style: solid;
111 | border-color: black;
112 | border-width: 0 1px 0 1px;
113 | }
114 |
115 | div.timerBarSection {
116 | height: 2px;
117 | position: absolute;
118 | background-color: gold;
119 | bottom: -2px;
120 | opacity: 1;
121 | border-style: solid;
122 | border-color: black;
123 | border-width: 0 1px 0 1px;
124 | }
125 |
126 | div.timestamps {
127 | display: flex;
128 | justify-content: space-between;
129 | margin: 2px 0 0 0;
130 | }
131 |
132 | div.timestamp {
133 | font-size: 14px;
134 | }
135 |
136 | div.playMarker {
137 | position: absolute;
138 | background-color: transparent;
139 | height: 15px;
140 | bottom: 0px;
141 | border-style: solid;
142 | border-color: white;
143 | border-width: 0 2px 0 0px;
144 | }
145 |
146 | div.timerBarCurrentPhrase{
147 | width: 100%;
148 | position: absolute;
149 | background-color: navy;
150 | bottom: 1px;
151 | opacity: .6;
152 | border-style: solid;
153 | border-color: black;
154 | border-width: 0 0px 0 0px;
155 | }
156 |
157 | /* MODE 1 */
158 | div.section {
159 | height: 2px;
160 | position: absolute;
161 | border-style: solid;
162 | border-color: black;
163 | border-width: 0 1px 0 1px;
164 | }
165 |
166 | div.phrase {
167 | height: 12px;
168 | position: absolute;
169 | border-style: solid;
170 | border-color: black;
171 | border-width: 0 1px 0 1px;
172 | }
173 |
174 | /* TRANSITIONS */
175 | .fade-expand-enter-active, .fade-expand-leave-active {
176 | transition: all 1s ease;
177 | }
178 | .fade-expand-enter, .fade-expand-leave-to {
179 | opacity: 0;
180 | max-height: 0;
181 | }
182 | .fade-expand-leave, .fade-expand-enter-to {
183 | max-height: 500px;
184 | }
185 |
186 | .fade-enter-active, .fade-leave-active {
187 | transition: opacity 1s ease;
188 | }
189 | .fade-enter, .fade-leave-to {
190 | opacity: 0;
191 | }
--------------------------------------------------------------------------------
/addons/note_streaks/style.css:
--------------------------------------------------------------------------------
1 | @keyframes popup-animation {
2 | /*start of animation*/
3 | 0% {
4 | opacity: 0;
5 | font-size: 100vw;
6 | transform: rotate(0deg);
7 | color: white;
8 | }
9 |
10 | /*End of entering animation and start of first bounce*/
11 | 10% {
12 | opacity: 1;
13 | font-size: 3.5vw;
14 | transform: rotate(0deg);
15 | }
16 |
17 | /*End of first bounce*/
18 | 25% {
19 | font-size: 7vw;
20 | transform: rotate(5deg);
21 | }
22 |
23 | /*Start of second bounce*/
24 | 35% {
25 | font-size: 3.5vw;
26 | transform: rotate(0deg);
27 | }
28 |
29 | /*End of second bounce*/
30 | 45% {
31 | font-size: 6vw;
32 | transform: rotate(-4deg);
33 | }
34 |
35 | /*Start of third bounce*/
36 | 50% {
37 | font-size: 3.5vw;
38 | transform: rotate(0deg);
39 | }
40 |
41 | /*End of third bounce*/
42 | 60% {
43 | font-size: 5vw;
44 | transform: rotate(2deg);
45 | }
46 |
47 | /*Start of fourth bounce*/
48 | 65% {
49 | font-size: 3.5vw;
50 | transform: rotate(0deg);
51 | }
52 |
53 | /*End of fourth bounce*/
54 | 75% {
55 | font-size: 4vw;
56 | transform: rotate(-1deg);
57 | }
58 |
59 | /*Start of exit animation*/
60 | 90% {
61 | opacity: 1;
62 | font-size: 3.5vw;
63 | transform: rotate(0deg) translate(0,0);
64 | }
65 |
66 | /*Final state and end of exit animation*/
67 | 100% {
68 | opacity: 0;
69 | transform: translateY(-100px);
70 | color: white;
71 | }
72 | }
73 |
74 | @keyframes popup-animation-rainbow {
75 | /*start of animation*/
76 | 0% {
77 | opacity: 0;
78 | font-size: 100vw;
79 | transform: rotate(0deg);
80 | }
81 |
82 | /*End of entering animation and start of first bounce*/
83 | 10% {
84 | opacity: 1;
85 | font-size: 3.5vw;
86 | transform: rotate(0deg);
87 | color: magenta;
88 | }
89 |
90 | /*End of first bounce*/
91 | 25% {
92 | font-size: 7vw;
93 | transform: rotate(5deg);
94 | color: red;
95 | }
96 |
97 | /*Start of second bounce*/
98 | 35% {
99 | font-size: 3.5vw;
100 | transform: rotate(0deg);
101 | color: DeepPink;
102 | }
103 |
104 | /*End of second bounce*/
105 | 45% {
106 | font-size: 6vw;
107 | transform: rotate(-4deg);
108 | color: yellow;
109 | }
110 |
111 | /*Start of third bounce*/
112 | 50% {
113 | font-size: 3.5vw;
114 | transform: rotate(0deg);
115 | color: green;
116 | }
117 |
118 | /*End of third bounce*/
119 | 60% {
120 | font-size: 5vw;
121 | transform: rotate(2deg);
122 | color: blue;
123 | }
124 |
125 | /*Start of fourth bounce*/
126 | 65% {
127 | font-size: 3.5vw;
128 | transform: rotate(0deg);
129 | color: aqua;
130 | }
131 |
132 | /*End of fourth bounce*/
133 | 75% {
134 | font-size: 4vw;
135 | transform: rotate(-1deg);
136 | color: purple;
137 | }
138 |
139 | /*Start of exit animation*/
140 | 90% {
141 | opacity: 1;
142 | font-size: 3.5vw;
143 | transform: rotate(0deg) translate(0,0);
144 | color: lime;
145 | }
146 |
147 | /*Final state and end of exit animation*/
148 | 100% {
149 | opacity: 0;
150 | transform: translateY(-100px);
151 | }
152 | }
153 |
154 | /* This applies to all the text */
155 | body {
156 | font-family: arial;
157 | }
158 |
159 | /* This is the popup container, hide it by default */
160 | div.popup {
161 | /*display: none;*/
162 | text-align: center;
163 | }
164 |
165 | /* This applies to the information text */
166 | h1 {
167 | font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
168 | font-size: 5vw;
169 | color: white;
170 | -webkit-text-stroke: 0.15vw black;
171 | }
172 |
173 | /* This applies to the header title text */
174 | h1.streak {
175 | animation-name: popup-animation;
176 | animation-duration: 3s;
177 | animation-timing-function: ease-in-out;
178 | animation-fill-mode: forwards;
179 | font-weight: bold;
180 | font-size: 7vw;
181 | color: white;
182 | padding-top: 22vw;
183 | display: none;
184 | }
--------------------------------------------------------------------------------
/addons/current_song_v3.1_LaS/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Lucida Console", Courier, monospace;
3 | font-size: 14px;
4 | }
5 |
6 | div.popup {
7 | width: 420px;
8 | display: flex;
9 | overflow: hidden;
10 | background-color: rgba(0,0,0,1);
11 | color: white;
12 | padding: 0px 0px 0px 0px;
13 | }
14 |
15 | div.mainContainer {
16 | width: 420px;
17 | }
18 |
19 | div.songInfo {
20 | white-space: nowrap;
21 | overflow: hidden;
22 | text-overflow: ellipsis;
23 | margin: 0px 5px 0px 5px;
24 | line-height: 20px;
25 |
26 | }
27 |
28 |
29 | div.songMarquee {
30 | transition: transform 2s ease-in-out;
31 | transform: translateX(0px);
32 | width: 100%;
33 | margin: 0px 0px 0px 0px;
34 | }
35 |
36 | a.songName {
37 | color: white;
38 | font-size: 18px;
39 | white-space: nowrap;
40 | }
41 |
42 | a.songDash {
43 | color: white;
44 | font-size: 18px;
45 | white-space: nowrap;
46 | }
47 |
48 | a.artistName {
49 | color: white;
50 | font-size: 18px;
51 | white-space: nowrap;
52 | }
53 |
54 | a.tuningName {
55 |
56 | }
57 |
58 | /* MODE 0 */
59 | img.albumArt {
60 | width: 100%;
61 | }
62 |
63 | div.stats{
64 | display: flex;
65 | width=100%;
66 | }
67 |
68 | div.statsLeft{
69 | display: flex;
70 | width: 50%;
71 | justify-content: space-between;
72 | margin: 0 5px 0 0;
73 | }
74 |
75 | div.statsRight{
76 | display: flex;
77 | width: 50%;
78 | justify-content: space-between;
79 | margin: 0 0 0 5px;
80 | }
81 |
82 | div.statsNAME{
83 | width: 25%;
84 | }
85 |
86 | div.statsNUM{
87 | font-size: 14px;
88 | width: 75%;
89 | text-align: right;
90 | }
91 | div.timer {
92 | margin: 2px 0 0 0;
93 | font-smooth: never;
94 | -webkit-font-smoothing : none;
95 | }
96 |
97 | div.timerBar {
98 | background-color: black;
99 | height: 13px;
100 | width: 100%;
101 | position: relative;
102 | border-width: 0 0 0 0;
103 | }
104 |
105 | div.timerBarPhrase{
106 | position: absolute;
107 | background-color: magenta;
108 | bottom: 1px;
109 | opacity: 1;
110 | border-style: solid;
111 | border-color: black;
112 | border-width: 0 1px 0 1px;
113 | }
114 |
115 | div.timerBarSection {
116 | height: 2px;
117 | position: absolute;
118 | background-color: gold;
119 | bottom: -2px;
120 | opacity: 1;
121 | border-style: solid;
122 | border-color: black;
123 | border-width: 0 1px 0 1px;
124 | }
125 |
126 | div.timestamps {
127 | display: flex;
128 | justify-content: space-between;
129 | margin: 2px 0 0 0;
130 | }
131 |
132 | div.timestamp {
133 | font-size: 14px;
134 | }
135 |
136 | div.playMarker {
137 | position: absolute;
138 | background-color: transparent;
139 | height: 15px;
140 | bottom: 0px;
141 | border-style: solid;
142 | border-color: white;
143 | border-width: 0 2px 0 0px;
144 | }
145 |
146 | div.timerBarCurrentPhrase{
147 | width: 100%;
148 | position: absolute;
149 | background-color: navy;
150 | bottom: 1px;
151 | opacity: .6;
152 | border-style: solid;
153 | border-color: black;
154 | border-width: 0 0px 0 0px;
155 | }
156 |
157 | /* MODE 1 */
158 | div.section {
159 | height: 2px;
160 | position: absolute;
161 | border-style: solid;
162 | border-color: black;
163 | border-width: 0 1px 0 1px;
164 | }
165 |
166 | div.phrase {
167 | height: 12px;
168 | position: absolute;
169 | border-style: solid;
170 | border-color: black;
171 | border-width: 0 1px 0 1px;
172 | }
173 |
174 | /* TRANSITIONS */
175 | .fade-expand-enter-active, .fade-expand-leave-active {
176 | transition: all 1s ease;
177 | }
178 | .fade-expand-enter, .fade-expand-leave-to {
179 | opacity: 0;
180 | max-height: 0;
181 | }
182 | .fade-expand-leave, .fade-expand-enter-to {
183 | max-height: 500px;
184 | }
185 |
186 | .fade-enter-active, .fade-leave-active {
187 | transition: opacity 1s ease;
188 | }
189 | .fade-enter, .fade-leave-to {
190 | opacity: 0;
191 | }
--------------------------------------------------------------------------------
/addons/Arcade_v1_LaS/Arcade.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PoizenJam's Rocksniffer UI
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/addons/Arcade_v1_LaS/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: PixelFont;
3 | src: url(ARCADE_N.ttf);
4 | }
5 |
6 | @keyframes slide {
7 | 0%{
8 | background-position-y: 0%;
9 | }
10 | 100%{
11 | background-position-y: 100%;
12 | }
13 | }
14 |
15 | body {
16 | font-family: PixelFont;
17 | font-size: 10px;
18 | font-smooth: never;
19 | -webkit-font-smoothing : none;
20 | }
21 |
22 | div.popup {
23 | width: 420px;
24 | display: flex;
25 | overflow: hidden;
26 | background-color: rgba(0,0,0,1);
27 | color: white;
28 | padding: 0px 0px 0px 0px;
29 | }
30 |
31 | div.mainContainer {
32 | width: 420px;
33 | }
34 |
35 | div.songInfo {
36 | white-space: nowrap;
37 | overflow: hidden;
38 | text-overflow: ellipsis;
39 | margin: 0px 5px 0px 5px;
40 | line-height: 16px;
41 |
42 | }
43 |
44 |
45 | div.songMarquee {
46 | transition: transform 4s ease-in-out;
47 | transform: translateX(0px);
48 | width: 100%;
49 | margin: 0px 0px 0px 0px;
50 | }
51 |
52 | a.songName {
53 | color: #fc00ff;
54 | font-size: 12px;
55 | white-space: nowrap;
56 | }
57 |
58 | a.songDash {
59 | color: white;
60 | font-size: 12px;
61 | white-space: nowrap;
62 | }
63 |
64 | a.artistName {
65 | color: #00fc00;
66 | font-size: 12px;
67 | white-space: nowrap;
68 | }
69 |
70 | a.tuningName {
71 |
72 | }
73 |
74 | /* MODE 0 */
75 | img.albumArt {
76 | width: 100%;
77 | }
78 |
79 |
80 |
81 | div.stats{
82 | display: flex;
83 | width=100%;
84 | }
85 |
86 | div.statsLeft{
87 | display: flex;
88 | width: 50%;
89 | justify-content: space-between;
90 | margin: 0 5px 0 0;
91 | }
92 |
93 | div.statsRight{
94 | display: flex;
95 | width: 50%;
96 | justify-content: space-between;
97 | margin: 0 0 0 5px;
98 | }
99 |
100 | div.statsNAME{
101 | }
102 |
103 | div.statsNUM{
104 | width: 90%;
105 | text-align: right;
106 | }
107 |
108 | div.scrDisplay{
109 | color: white;
110 | }
111 |
112 | div.hitDisplay{
113 | color: white;
114 | }
115 |
116 | div.mltDisplay{
117 | color: white;
118 | }
119 |
120 | div.strDisplay{
121 | color: white;
122 | }
123 |
124 | div.timer {
125 | margin: 2px 0 0 0;
126 | font-smooth: never;
127 | -webkit-font-smoothing : none;
128 | }
129 |
130 | div.timerBar {
131 | background-color: black;
132 | height: 13px;
133 | width: 100%;
134 | position: relative;
135 | border-width: 0 0 0 0;
136 | }
137 |
138 | div.timerBarPhrase{
139 | position: absolute;
140 | background-color: #fc00ff;
141 | bottom: 1px;
142 | opacity: 1;
143 | border-style: solid;
144 | border-color: black;
145 | border-width: 0 1px 0 1px;
146 | }
147 |
148 | div.timerBarSection {
149 | height: 2px;
150 | position: absolute;
151 | background-color: #fcd800;
152 | bottom: -2px;
153 | opacity: 1;
154 | border-style: solid;
155 | border-color: black;
156 | border-width: 0 1px 0 1px;
157 | }
158 |
159 | div.timestamps {
160 | display: flex;
161 | justify-content: space-between;
162 | margin: 2px 0 0 0;
163 | }
164 |
165 | div.timestamp {
166 | font-size: 14px;
167 | }
168 |
169 | div.playMarker {
170 | position: absolute;
171 | background-color: transparent;
172 | height: 15px;
173 | bottom: 0px;
174 | border-style: solid;
175 | border-color: white;
176 | border-width: 0 2px 0 0px;
177 | }
178 |
179 | div.timerBarCurrentPhrase{
180 | width: 100%;
181 | position: absolute;
182 | background-color: #6c00aa;
183 | bottom: 1px;
184 | opacity: 1;
185 | border-style: solid;
186 | border-color: black;
187 | border-width: 0 0px 0 0px;
188 | }
189 |
190 | /* MODE 1 */
191 | div.section {
192 | height: 2px;
193 | position: absolute;
194 | border-style: solid;
195 | border-color: black;
196 | border-width: 0 1px 0 1px;
197 | }
198 |
199 | div.phrase {
200 | height: 12px;
201 | position: absolute;
202 | border-style: solid;
203 | border-color: black;
204 | border-width: 0 1px 0 1px;
205 | }
206 |
207 | /* TRANSITIONS */
208 | .fade-expand-enter-active, .fade-expand-leave-active {
209 | transition: all 1s ease;
210 | }
211 | .fade-expand-enter, .fade-expand-leave-to {
212 | opacity: 0;
213 | max-height: 0;
214 | }
215 | .fade-expand-leave, .fade-expand-enter-to {
216 | max-height: 500px;
217 | }
218 |
219 | .fade-enter-active, .fade-leave-active {
220 | transition: opacity 1s ease;
221 | }
222 | .fade-enter, .fade-leave-to {
223 | opacity: 0;
224 | }
--------------------------------------------------------------------------------
/addons/Arcade_v1_SA/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: PixelFont;
3 | src: url(ARCADE_N.ttf);
4 | }
5 |
6 |
7 | @keyframes slide {
8 | 0%{
9 | background-position-y: 0%;
10 | }
11 | 100%{
12 | background-position-y: 100%;
13 | }
14 | }
15 |
16 | body {
17 | font-family: PixelFont;
18 | font-size: 10px;
19 | font-smooth: never;
20 | -webkit-font-smoothing : none;
21 | }
22 |
23 | div.popup {
24 | width: 420px;
25 | display: flex;
26 | overflow: hidden;
27 | background-color: rgba(0,0,0,1);
28 | color: white;
29 | padding: 0px 0px 0px 0px;
30 | }
31 |
32 | div.mainContainer {
33 | width: 420px;
34 | }
35 |
36 | div.songInfo {
37 | white-space: nowrap;
38 | overflow: hidden;
39 | text-overflow: ellipsis;
40 | margin: 0px 5px 0px 5px;
41 | line-height: 16px;
42 |
43 | }
44 |
45 |
46 | div.songMarquee {
47 | transition: transform 4s ease-in-out;
48 | transform: translateX(0px);
49 | width: 100%;
50 | margin: 0px 0px 0px 0px;
51 | }
52 |
53 | a.songName {
54 | color: #fc00ff;
55 | font-size: 12px;
56 | white-space: nowrap;
57 | }
58 |
59 | a.songDash {
60 | color: white;
61 | font-size: 12px;
62 | white-space: nowrap;
63 | }
64 |
65 | a.artistName {
66 | color: #00fc00;
67 | font-size: 12px;
68 | white-space: nowrap;
69 | }
70 |
71 | a.tuningName {
72 |
73 | }
74 |
75 | /* MODE 0 */
76 | img.albumArt {
77 | width: 100%;
78 | }
79 |
80 |
81 |
82 | div.stats{
83 | display: flex;
84 | width=100%;
85 | }
86 |
87 | div.statsLeft{
88 | display: flex;
89 | width: 50%;
90 | justify-content: space-between;
91 | margin: 0 5px 0 0;
92 | }
93 |
94 | div.statsRight{
95 | display: flex;
96 | width: 50%;
97 | justify-content: space-between;
98 | margin: 0 0 0 5px;
99 | }
100 |
101 | div.statsNAME{
102 | }
103 |
104 | div.statsNUM{
105 | width: 90%;
106 | text-align: right;
107 | }
108 |
109 | div.scrDisplay{
110 | color: white;
111 | }
112 |
113 | div.hitDisplay{
114 | color: white;
115 | }
116 |
117 | div.mltDisplay{
118 | color: white;
119 | }
120 |
121 | div.strDisplay{
122 | color: white;
123 | }
124 |
125 | div.timer {
126 | margin: 2px 0 0 0;
127 | font-smooth: never;
128 | -webkit-font-smoothing : none;
129 | }
130 |
131 | div.timerBar {
132 | background-color: black;
133 | height: 13px;
134 | width: 100%;
135 | position: relative;
136 | border-width: 0 0 0 0;
137 | }
138 |
139 | div.timerBarPhrase{
140 | position: absolute;
141 | background-color: #fc00ff;
142 | bottom: 1px;
143 | opacity: 1;
144 | border-style: solid;
145 | border-color: black;
146 | border-width: 0 1px 0 1px;
147 | }
148 |
149 | div.timerBarSection {
150 | height: 2px;
151 | position: absolute;
152 | background-color: #fcd800;
153 | bottom: -2px;
154 | opacity: 1;
155 | border-style: solid;
156 | border-color: black;
157 | border-width: 0 1px 0 1px;
158 | }
159 |
160 | div.timestamps {
161 | display: flex;
162 | justify-content: space-between;
163 | margin: 2px 0 0 0;
164 | }
165 |
166 | div.timestamp {
167 | font-size: 14px;
168 | }
169 |
170 | div.playMarker {
171 | position: absolute;
172 | background-color: transparent;
173 | height: 15px;
174 | bottom: 0px;
175 | border-style: solid;
176 | border-color: white;
177 | border-width: 0 2px 0 0px;
178 | }
179 |
180 | div.timerBarCurrentPhrase{
181 | width: 100%;
182 | position: absolute;
183 | background-color: #6c00aa;
184 | bottom: 1px;
185 | opacity: 1;
186 | border-style: solid;
187 | border-color: black;
188 | border-width: 0 0px 0 0px;
189 | }
190 |
191 | /* MODE 1 */
192 | div.section {
193 | height: 2px;
194 | position: absolute;
195 | border-style: solid;
196 | border-color: black;
197 | border-width: 0 1px 0 1px;
198 | }
199 |
200 | div.phrase {
201 | height: 12px;
202 | position: absolute;
203 | border-style: solid;
204 | border-color: black;
205 | border-width: 0 1px 0 1px;
206 | }
207 |
208 | /* TRANSITIONS */
209 | .fade-expand-enter-active, .fade-expand-leave-active {
210 | transition: all 1s ease;
211 | }
212 | .fade-expand-enter, .fade-expand-leave-to {
213 | opacity: 0;
214 | max-height: 0;
215 | }
216 | .fade-expand-leave, .fade-expand-enter-to {
217 | max-height: 500px;
218 | }
219 |
220 | .fade-enter-active, .fade-leave-active {
221 | transition: opacity 1s ease;
222 | }
223 | .fade-enter, .fade-leave-to {
224 | opacity: 0;
225 | }
--------------------------------------------------------------------------------
/addons/current_song_v3.1_LaS/current_song_v3.1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Current Song Display (Based on v2, edited by PoizenJam)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | [Xx]64/
19 | [Xx]86/
20 | [Bb]uild/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 |
85 | # Visual Studio profiler
86 | *.psess
87 | *.vsp
88 | *.vspx
89 | *.sap
90 |
91 | # TFS 2012 Local Workspace
92 | $tf/
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 | *.DotSettings.user
101 |
102 | # JustCode is a .NET coding add-in
103 | .JustCode
104 |
105 | # TeamCity is a build add-in
106 | _TeamCity*
107 |
108 | # DotCover is a Code Coverage Tool
109 | *.dotCover
110 |
111 | # NCrunch
112 | _NCrunch_*
113 | .*crunch*.local.xml
114 | nCrunchTemp_*
115 |
116 | # MightyMoose
117 | *.mm.*
118 | AutoTest.Net/
119 |
120 | # Web workbench (sass)
121 | .sass-cache/
122 |
123 | # Installshield output folder
124 | [Ee]xpress/
125 |
126 | # DocProject is a documentation generator add-in
127 | DocProject/buildhelp/
128 | DocProject/Help/*.HxT
129 | DocProject/Help/*.HxC
130 | DocProject/Help/*.hhc
131 | DocProject/Help/*.hhk
132 | DocProject/Help/*.hhp
133 | DocProject/Help/Html2
134 | DocProject/Help/html
135 |
136 | # Click-Once directory
137 | publish/
138 |
139 | # Publish Web Output
140 | *.[Pp]ublish.xml
141 | *.azurePubxml
142 |
143 | # TODO: Un-comment the next line if you do not want to checkin
144 | # your web deploy settings because they may include unencrypted
145 | # passwords
146 | #*.pubxml
147 | *.publishproj
148 |
149 | # NuGet Packages
150 | *.nupkg
151 | # The packages folder can be ignored because of Package Restore
152 | **/packages/*
153 | # except build/, which is used as an MSBuild target.
154 | !**/packages/build/
155 | # Uncomment if necessary however generally it will be regenerated when needed
156 | #!**/packages/repositories.config
157 | # NuGet v3's project.json files produces more ignoreable files
158 | *.nuget.props
159 | *.nuget.targets
160 |
161 | # Microsoft Azure Build Output
162 | csx/
163 | *.build.csdef
164 |
165 | # Microsoft Azure Emulator
166 | ecf/
167 | rcf/
168 |
169 | # Windows Store app package directory
170 | AppPackages/
171 | BundleArtifacts/
172 |
173 | # Visual Studio cache files
174 | # files ending in .cache can be ignored
175 | *.[Cc]ache
176 | # but keep track of directories ending in .cache
177 | !*.[Cc]ache/
178 |
179 | # Others
180 | ClientBin/
181 | [Ss]tyle[Cc]op.*
182 | ~$*
183 | *~
184 | *.dbmdl
185 | *.dbproj.schemaview
186 | *.pfx
187 | *.publishsettings
188 | node_modules/
189 | orleans.codegen.cs
190 |
191 | # RIA/Silverlight projects
192 | Generated_Code/
193 |
194 | # Backup & report files from converting an old project file
195 | # to a newer Visual Studio version. Backup files are not needed,
196 | # because we have git ;-)
197 | _UpgradeReport_Files/
198 | Backup*/
199 | UpgradeLog*.XML
200 | UpgradeLog*.htm
201 |
202 | # SQL Server files
203 | *.mdf
204 | *.ldf
205 |
206 | # Business Intelligence projects
207 | *.rdl.data
208 | *.bim.layout
209 | *.bim_*.settings
210 |
211 | # Microsoft Fakes
212 | FakesAssemblies/
213 |
214 | # GhostDoc plugin setting file
215 | *.GhostDoc.xml
216 |
217 | # Node.js Tools for Visual Studio
218 | .ntvs_analysis.dat
219 |
220 | # Visual Studio 6 build log
221 | *.plg
222 |
223 | # Visual Studio 6 workspace options file
224 | *.opt
225 |
226 | # Visual Studio LightSwitch build output
227 | **/*.HTMLClient/GeneratedArtifacts
228 | **/*.DesktopClient/GeneratedArtifacts
229 | **/*.DesktopClient/ModelManifest.xml
230 | **/*.Server/GeneratedArtifacts
231 | **/*.Server/ModelManifest.xml
232 | _Pvt_Extensions
233 |
234 | # LightSwitch generated files
235 | GeneratedArtifacts/
236 | ModelManifest.xml
237 |
238 | # Paket dependency manager
239 | .paket/paket.exe
240 |
241 | # FAKE - F# Make
242 | .fake/
243 | .idea
244 |
--------------------------------------------------------------------------------
/RockSniffer/RPC/AlbumArtResolver.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using RockSnifferLib.Logging;
3 | using RockSnifferLib.Sniffing;
4 | using System;
5 | using System.Collections.Concurrent;
6 | using System.Collections.Specialized;
7 | using System.IO;
8 | using System.Net;
9 | using System.Net.Http;
10 |
11 | namespace RockSniffer.RPC
12 | {
13 | public class AlbumArtResolver
14 | {
15 | [Serializable]
16 | private class AppleResult
17 | {
18 | public string artistName;
19 | public string collectionName;
20 | public string trackName;
21 | public string artworkUrl100;
22 | }
23 |
24 | [Serializable]
25 | private class AppleRequestResponse
26 | {
27 | public int resultCount;
28 | public AppleResult[] results;
29 | }
30 |
31 | private HttpClient httpClient = new HttpClient();
32 | private ConcurrentDictionary cache = new ConcurrentDictionary();
33 |
34 |
35 | public AlbumArtResolver() {
36 | httpClient.DefaultRequestHeaders.Add("User-Agent", "RockSniffer");
37 | }
38 |
39 | ///
40 | /// Attempt to obtain album cover
41 | ///
42 | /// Details about the given song
43 | public (string URL, string DisplayText)? Get(SongDetails songInfo)
44 | {
45 | string key = $"{songInfo.artistName}|{songInfo.albumName}";
46 |
47 | cache.AddOrUpdate(key, (key) => GetFromAppleMusic(songInfo), (key, value) => value);
48 |
49 | return cache[key];
50 | }
51 |
52 | ///
53 | /// Attempt to obtain album cover from Apple Music
54 | ///
55 | /// Details about the given song
56 | protected (string URL, string DisplayText)? GetFromAppleMusic(SongDetails songInfo)
57 | {
58 | string baseUrl = "https://itunes.apple.com/search";
59 | NameValueCollection queryString = System.Web.HttpUtility.ParseQueryString(string.Empty);
60 | queryString.Add("media", "music");
61 | queryString.Add("limit", "25");
62 | queryString.Add("term", $"{songInfo.artistName} {songInfo.songName}");
63 |
64 | var webRequest = new HttpRequestMessage(HttpMethod.Get, baseUrl + "?" + queryString.ToString());
65 |
66 | HttpResponseMessage responseMessage = httpClient.Send(webRequest);
67 |
68 | if (responseMessage.StatusCode != HttpStatusCode.OK)
69 | return null;
70 |
71 | using (var reader = new StreamReader(responseMessage.Content.ReadAsStream()))
72 | {
73 | // Attempt deserialization, return null on failure
74 | AppleRequestResponse? responseOpt = null;
75 | try
76 | {
77 | responseOpt = JsonConvert.DeserializeObject(reader.ReadToEnd());
78 | }
79 | catch (Exception ex)
80 | {
81 | Logger.LogException(ex);
82 | return null;
83 | }
84 |
85 | if (responseOpt is AppleRequestResponse response)
86 | {
87 | AppleResult? bestResult = null;
88 |
89 | // Attempt to find a full match, or at least valid result
90 | foreach (AppleResult appleResult in response.results)
91 | {
92 | if (appleResult.artistName != null && appleResult.collectionName != null && appleResult.trackName != null)
93 | {
94 | if (bestResult == null)
95 | bestResult = appleResult;
96 |
97 | if ((appleResult.artistName.Contains(songInfo.artistName, StringComparison.OrdinalIgnoreCase)
98 | || songInfo.artistName.Contains(appleResult.artistName, StringComparison.OrdinalIgnoreCase))
99 | && (appleResult.collectionName.Contains(songInfo.albumName, StringComparison.OrdinalIgnoreCase)
100 | || songInfo.albumName.Contains(appleResult.collectionName, StringComparison.OrdinalIgnoreCase)))
101 | {
102 | bestResult = appleResult;
103 | break;
104 | }
105 | }
106 | }
107 |
108 | // If bestResult isn't null (is valid result, perhaps even full match), return the URL and description
109 | if (bestResult is AppleResult bestResultOk)
110 | {
111 | return (bestResultOk.artworkUrl100, $"{bestResultOk.artistName} - {bestResultOk.collectionName} (art from Apple Music)");
112 | }
113 | }
114 |
115 | // Return null in case response had no data or so
116 | return null;
117 | };
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/addons/Arcade_v1_SA/Arcade.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PoizenJam's Rocksniffer UI
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/addons/timeline/timeline.css:
--------------------------------------------------------------------------------
1 | div.timeline-frame {
2 | -moz-box-sizing: border-box;
3 | border: 1px solid #bebebe;
4 | box-sizing: border-box;
5 | overflow: hidden;
6 | position: relative;
7 | }
8 |
9 | div.timeline-content {
10 | overflow: hidden;
11 | position: relative;
12 | }
13 |
14 | div.timeline-axis {
15 | -moz-box-sizing: border-box;
16 | border-color: #bebebe;
17 | border-top-style: solid;
18 | border-width: 1px;
19 | box-sizing: border-box;
20 | }
21 |
22 | div.timeline-axis-grid {
23 | -moz-box-sizing: border-box;
24 | border-left-style: solid;
25 | border-width: 1px;
26 | box-sizing: border-box;
27 | }
28 |
29 | div.timeline-axis-grid-minor {
30 | border-color: #e5e5e5;
31 | }
32 |
33 | div.timeline-axis-grid-major {
34 | border-color: #bfbfbf;
35 | }
36 |
37 | div.timeline-axis-text {
38 | color: #4d4d4d;
39 | padding: 3px;
40 | white-space: nowrap;
41 | }
42 |
43 | div.timeline-axis-text-minor {
44 | }
45 |
46 | div.timeline-axis-text-major {
47 | }
48 |
49 | div.timeline-event {
50 | -moz-box-sizing: border-box;
51 | background-color: #d5ddf6;
52 | border-color: #97b0f8;
53 | box-sizing: border-box;
54 | color: #1a1a1a;
55 | display: inline-block;
56 | }
57 |
58 | div.timeline-event-selected {
59 | background-color: #fff785;
60 | border-color: #ffc200;
61 | z-index: 999;
62 | }
63 |
64 | /* TODO: use another color or pattern? */
65 | div.timeline-event-cluster {
66 | background: url('img/cluster_bg.png') #97b0f8;
67 | color: #ffffff;
68 | }
69 |
70 | div.timeline-event-cluster div.timeline-event-dot {
71 | border-color: #d5ddf6;
72 | }
73 |
74 | div.timeline-event-box {
75 | -moz-border-radius: 5px; /* For Firefox 3.6 and older */
76 | border-radius: 5px;
77 | border-style: solid;
78 | border-width: 1px;
79 | text-align: center;
80 | }
81 |
82 | div.timeline-event-dot {
83 | -moz-border-radius: 5px; /* For Firefox 3.6 and older */
84 | border-radius: 5px;
85 | border-style: solid;
86 | border-width: 5px;
87 | }
88 |
89 | div.timeline-event-range {
90 | -moz-border-radius: 2px; /* For Firefox 3.6 and older */
91 | border-radius: 2px;
92 | border-style: solid;
93 | border-width: 1px;
94 | }
95 |
96 | div.timeline-event-range-drag-left {
97 | cursor: w-resize;
98 | z-index: 1000;
99 | }
100 |
101 | div.timeline-event-range-drag-right {
102 | cursor: e-resize;
103 | z-index: 1000;
104 | }
105 |
106 | div.timeline-event-line {
107 | -moz-box-sizing: border-box;
108 | border-left-style: solid;
109 | border-left-width: 1px;
110 | box-sizing: border-box;
111 | }
112 |
113 | div.timeline-event-content {
114 | margin: 5px;
115 | overflow: hidden;
116 | white-space: nowrap;
117 | }
118 |
119 | div.timeline-groups-axis {
120 | -moz-box-sizing: border-box;
121 | border-color: #bebebe;
122 | border-width: 1px;
123 | box-sizing: border-box;
124 | }
125 |
126 | div.timeline-groups-axis-onleft {
127 | border-style: none solid none none;
128 | }
129 |
130 | div.timeline-groups-axis-onright {
131 | border-style: none none none solid;
132 | }
133 |
134 | div.timeline-groups-text {
135 | color: #4d4d4d;
136 | padding-left: 10px;
137 | padding-right: 10px;
138 | }
139 |
140 | div.timeline-currenttime {
141 | -moz-box-sizing: border-box;
142 | background-color: #ff7f6e;
143 | box-sizing: border-box;
144 | width: 2px;
145 | }
146 |
147 | div.timeline-customtime {
148 | -moz-box-sizing: border-box;
149 | background-color: #6e94ff;
150 | box-sizing: border-box;
151 | cursor: move;
152 | width: 2px;
153 | }
154 |
155 | div.timeline-navigation {
156 | -moz-border-radius: 2px; /* For Firefox 3.6 and older */
157 | -moz-box-sizing: border-box;
158 | background-color: #f5f5f5;
159 | border: 1px solid #bebebe;
160 | border-radius: 2px;
161 | box-sizing: border-box;
162 | color: #808080;
163 | font-family: arial;
164 | font-size: 20px;
165 | font-weight: bold;
166 | }
167 |
168 | div.timeline-navigation-new,
169 | div.timeline-navigation-delete,
170 | div.timeline-navigation-zoom-in,
171 | div.timeline-navigation-zoom-out,
172 | div.timeline-navigation-move-left,
173 | div.timeline-navigation-move-right {
174 | -moz-box-sizing: border-box;
175 | box-sizing: border-box;
176 | cursor: pointer;
177 | float: left;
178 | height: 36px;
179 | padding: 10px;
180 | text-decoration: none;
181 | width: 36px;
182 | }
183 |
184 | div.timeline-navigation-new {
185 | background: url('img/16/new.png') no-repeat center;
186 | }
187 |
188 | /* separator between new and navigation buttons */
189 | div.timeline-navigation-new-line {
190 | border-right: 1px solid #bebebe;
191 | }
192 |
193 | div.timeline-navigation-delete {
194 | background: url('img/16/delete.png') no-repeat center;
195 | }
196 |
197 | div.timeline-navigation-zoom-in {
198 | background: url('img/16/zoomin.png') no-repeat center;
199 | }
200 |
201 | div.timeline-navigation-zoom-out {
202 | background: url('img/16/zoomout.png') no-repeat center;
203 | }
204 |
205 | div.timeline-navigation-move-left {
206 | background: url('img/16/moveleft.png') no-repeat center;
207 | }
208 |
209 | div.timeline-navigation-move-right {
210 | background: url('img/16/moveright.png') no-repeat center;
211 | }
212 |
--------------------------------------------------------------------------------
/addons/current_song_v3/current_song_v3.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | RockSniffer Current Song Display (Based on v2, edited by PoizenJam)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/addons/_deps/jquery.animateNumber.js:
--------------------------------------------------------------------------------
1 | /** @preserve jQuery animateNumber plugin v0.0.14
2 | * (c) 2013, Alexandr Borisov.
3 | * https://github.com/aishek/jquery-animateNumber
4 | */
5 |
6 | // ['...'] notation using to avoid names minification by Google Closure Compiler
7 | (function($) {
8 | var reverse = function(value) {
9 | return value.split('').reverse().join('');
10 | };
11 |
12 | var defaults = {
13 | numberStep: function(now, tween) {
14 | var floored_number = Math.floor(now),
15 | target = $(tween.elem);
16 |
17 | target.text(floored_number);
18 | }
19 | };
20 |
21 | var handle = function( tween ) {
22 | var elem = tween.elem;
23 | if ( elem.nodeType && elem.parentNode ) {
24 | var handler = elem._animateNumberSetter;
25 | if (!handler) {
26 | handler = defaults.numberStep;
27 | }
28 | handler(tween.now, tween);
29 | }
30 | };
31 |
32 | if (!$.Tween || !$.Tween.propHooks) {
33 | $.fx.step.number = handle;
34 | } else {
35 | $.Tween.propHooks.number = {
36 | set: handle
37 | };
38 | }
39 |
40 | var extract_number_parts = function(separated_number, group_length) {
41 | var numbers = separated_number.split('').reverse(),
42 | number_parts = [],
43 | current_number_part,
44 | current_index,
45 | q;
46 |
47 | for(var i = 0, l = Math.ceil(separated_number.length / group_length); i < l; i++) {
48 | current_number_part = '';
49 | for(q = 0; q < group_length; q++) {
50 | current_index = i * group_length + q;
51 | if (current_index === separated_number.length) {
52 | break;
53 | }
54 |
55 | current_number_part = current_number_part + numbers[current_index];
56 | }
57 | number_parts.push(current_number_part);
58 | }
59 |
60 | return number_parts;
61 | };
62 |
63 | var remove_precending_zeros = function(number_parts) {
64 | var last_index = number_parts.length - 1,
65 | last = reverse(number_parts[last_index]);
66 |
67 | number_parts[last_index] = reverse(parseInt(last, 10).toString());
68 | return number_parts;
69 | };
70 |
71 | $.animateNumber = {
72 | numberStepFactories: {
73 | /**
74 | * Creates numberStep handler, which appends string to floored animated number on each step.
75 | *
76 | * @example
77 | * // will animate to 100 with "1 %", "2 %", "3 %", ...
78 | * $('#someid').animateNumber({
79 | * number: 100,
80 | * numberStep: $.animateNumber.numberStepFactories.append(' %')
81 | * });
82 | *
83 | * @params {String} suffix string to append to animated number
84 | * @returns {Function} numberStep-compatible function for use in animateNumber's parameters
85 | */
86 | append: function(suffix) {
87 | return function(now, tween) {
88 | var floored_number = Math.floor(now),
89 | target = $(tween.elem);
90 |
91 | target.prop('number', now).text(floored_number + suffix);
92 | };
93 | },
94 |
95 | /**
96 | * Creates numberStep handler, which format floored numbers by separating them to groups.
97 | *
98 | * @example
99 | * // will animate with 1 ... 217,980 ... 95,217,980 ... 7,095,217,980
100 | * $('#world-population').animateNumber({
101 | * number: 7095217980,
102 | * numberStep: $.animateNumber.numberStepFactories.separator(',')
103 | * });
104 | * @example
105 | * // will animate with 1% ... 217,980% ... 95,217,980% ... 7,095,217,980%
106 | * $('#salesIncrease').animateNumber({
107 | * number: 7095217980,
108 | * numberStep: $.animateNumber.numberStepFactories.separator(',', 3, '%')
109 | * });
110 | *
111 | * @params {String} [separator=' '] string to separate number groups
112 | * @params {String} [group_length=3] number group length
113 | * @params {String} [suffix=''] suffix to append to number
114 | * @returns {Function} numberStep-compatible function for use in animateNumber's parameters
115 | */
116 | separator: function(separator, group_length, suffix) {
117 | separator = separator || ' ';
118 | group_length = group_length || 3;
119 | suffix = suffix || '';
120 |
121 | return function(now, tween) {
122 | var negative = now < 0,
123 | floored_number = Math.floor((negative ? -1 : 1) * now),
124 | separated_number = floored_number.toString(),
125 | target = $(tween.elem);
126 |
127 | if (separated_number.length > group_length) {
128 | var number_parts = extract_number_parts(separated_number, group_length);
129 |
130 | separated_number = remove_precending_zeros(number_parts).join(separator);
131 | separated_number = reverse(separated_number);
132 | }
133 |
134 | target.prop('number', now).text((negative ? '-' : '') + separated_number + suffix);
135 | };
136 | }
137 | }
138 | };
139 |
140 | $.fn.animateNumber = function() {
141 | var options = arguments[0],
142 | settings = $.extend({}, defaults, options),
143 |
144 | target = $(this),
145 | args = [settings];
146 |
147 | for(var i = 1, l = arguments.length; i < l; i++) {
148 | args.push(arguments[i]);
149 | }
150 |
151 | // needs of custom step function usage
152 | if (options.numberStep) {
153 | // assigns custom step functions
154 | var items = this.each(function(){
155 | this._animateNumberSetter = options.numberStep;
156 | });
157 |
158 | // cleanup of custom step functions after animation
159 | var generic_complete = settings.complete;
160 | settings.complete = function() {
161 | items.each(function(){
162 | delete this._animateNumberSetter;
163 | });
164 |
165 | if ( generic_complete ) {
166 | generic_complete.apply(this, arguments);
167 | }
168 | };
169 | }
170 |
171 | return target.animate.apply(target, args);
172 | };
173 |
174 | }(jQuery));
175 |
--------------------------------------------------------------------------------
/addons/vocals/script.js:
--------------------------------------------------------------------------------
1 | let poller = new SnifferPoller({
2 |
3 | // Polling interval in ms
4 | interval: 30,
5 |
6 | // Latency compensation in second
7 | // If the lyrics are displayed with a delay, this value must be increased
8 | // It depends on the power of your PC and the usage of your network
9 | latencyCompensation: 0.250,
10 |
11 | // Time in second for display vocal before lyric start
12 | preVocalDisplayTime: 2,
13 | // Time in second for display vocal after lyric end
14 | postVocalDisplayTime: 0.7,
15 | // Number of line displayed, This value can be exceeded when it sings fast
16 | numberOfLinesDisplayed: 2,
17 |
18 | // Max note kepping, Use for compatibility with bad length of vocal
19 | maxNoteKepping: 2.5,
20 |
21 | // HTML / CSS customization
22 | beginLyric: '',
23 | endLyric: '',
24 | newLine: '
',
25 |
26 | onData: function(data) {
27 |
28 | // Not in song
29 | if (data.currentState != STATE_SONG_PLAYING) {
30 | $(".vocal").html("");
31 | return;
32 | }
33 |
34 | let currentTime = data.memoryReadout.songTimer + this.latencyCompensation;
35 | let vocals = data.songDetails.vocals;
36 |
37 | // Song not started or song without vocals
38 | if (currentTime <= 0 || vocals == null) {
39 | $(".vocal").html("");
40 | return;
41 | }
42 |
43 | // Vocals exist but is empty
44 | let vocalsLength = vocals.length;
45 | if (vocalsLength <= 0) {
46 | $(".vocal").html("");
47 | return;
48 | }
49 |
50 | // Search pre index. Only for performance
51 | let part = vocalsLength / 2;
52 | let index = Math.floor(part);
53 | while (true) {
54 | part /= 2;
55 | if (vocals[index].Time <= currentTime) {
56 | index = Math.floor(index + part);
57 |
58 | } else if (vocals[index].Time > currentTime) {
59 | index = Math.floor(index - part);
60 | }
61 | if (part <= 2 || index <= 0 || index >= vocalsLength) {
62 | break;
63 | }
64 | }
65 | // Start 40 syllables before current vocal
66 | // 30 is not enought for japanese language
67 | index -= 40;
68 | if (index <= 0) {
69 | index = 0;
70 | }
71 |
72 | // State
73 | let isNextNewLine;
74 | let isNextWithoutSpace;
75 | let isEndOfCurrent = false;
76 | let isNewLineBefore = false;
77 | let stopAtNextLine = false;
78 |
79 | let vocal;
80 | let lyric;
81 | let noteKeeping;
82 | let timeDifference;
83 | let currentLine = this.beginLyric;
84 | let lineNumber = 1;
85 |
86 | const regExStartWithUpperCaseChar = /[A-Z]/;
87 |
88 | // TODO don't create a new line when it's start whith "I " or "I'" and the preceded word have a majuscule
89 | // only in two compatibility process
90 |
91 | for (; index < vocalsLength; ++index) {
92 |
93 | // Get state from vocal
94 | vocal = vocals[index];
95 | lyric = vocal.Lyric;
96 | noteKeeping = vocal.Length;
97 | if (noteKeeping > this.maxNoteKepping) {
98 | noteKeeping = this.maxNoteKepping;
99 | }
100 |
101 | isNextNewLine = false;
102 | isNextWithoutSpace = false;
103 | if (lyric.endsWith("-")) {
104 | isNextWithoutSpace = true;
105 | } else if (lyric.endsWith("+")) {
106 | isNextNewLine = true;
107 | }
108 |
109 | // Remove vocal state from lyric
110 | if (isNextWithoutSpace || isNextNewLine) {
111 | lyric = lyric.substr(0, lyric.length-1);
112 | }
113 |
114 | timeDifference = currentTime - vocal.Time;
115 | if (timeDifference >= 0) {
116 |
117 | // Compatibility with Rocksmith song without state in vocal
118 | if (regExStartWithUpperCaseChar.test(lyric.substr(0, 1))) {
119 | if (!currentLine.endsWith(this.newLine) && currentLine != this.beginLyric) {
120 | isNewLineBefore = false;
121 | currentLine = this.beginLyric;
122 | lineNumber = 1;
123 | }
124 | }
125 |
126 | // Do not display vocals too early
127 | if (timeDifference + this.preVocalDisplayTime < 0) {
128 | if (currentLine == this.beginLyric || currentLine.endsWith(this.newLine)) {
129 | break;
130 | }
131 | stopAtNextLine = true;
132 | }
133 |
134 | // Vocal before current time
135 | currentLine = currentLine + lyric;
136 | if (isNextNewLine) {
137 | // If new line exist before or current vocal is during a lyric who was not terminated
138 | // and when is finish display a post time defined
139 | if ((isNewLineBefore || (timeDifference - noteKeeping) >= 0) && (timeDifference - noteKeeping - this.postVocalDisplayTime) >= 0) {
140 | isNewLineBefore = false;
141 | currentLine = this.beginLyric;
142 | lineNumber = 1;
143 | } else {
144 | if (stopAtNextLine) {
145 | break;
146 | }
147 | isNewLineBefore = true;
148 | currentLine += this.newLine;
149 | ++lineNumber;
150 | }
151 | } else if (!isNextWithoutSpace) {
152 | currentLine += ' ';
153 | }
154 |
155 | } else {
156 | // Compatibility with Rocksmith song without state in vocal
157 | if (regExStartWithUpperCaseChar.test(lyric.substr(0, 1))) {
158 | if (!currentLine.endsWith(this.newLine) && currentLine != this.beginLyric) {
159 | if (lineNumber >= this.numberOfLinesDisplayed) {
160 | break;
161 | }
162 | currentLine += this.newLine;
163 | ++lineNumber;
164 | }
165 | }
166 |
167 | // Do not display vocals too early
168 | if (timeDifference + this.preVocalDisplayTime < 0) {
169 | if (currentLine == this.beginLyric || currentLine.endsWith(this.newLine)) {
170 | break;
171 | }
172 | stopAtNextLine = true;
173 | }
174 | if (!isEndOfCurrent) {
175 | currentLine += this.endLyric;
176 | isEndOfCurrent = true;
177 | }
178 | currentLine = currentLine + lyric;
179 | if (isNextNewLine) {
180 | if (stopAtNextLine) {
181 | break;
182 | }
183 | currentLine += this.newLine;
184 | ++lineNumber;
185 | } else if (!isNextWithoutSpace) {
186 | currentLine += ' ';
187 | }
188 | // Limit the number of line displayed
189 | if (lineNumber >= this.numberOfLinesDisplayed) {
190 | stopAtNextLine = true;
191 | }
192 | }
193 | }
194 | $(".vocal").html(currentLine);
195 | },
196 | onSongChanged(f) {
197 | $(".vocal").html("");
198 | },
199 | onSongStarted(f) {
200 | $(".vocal").html("");
201 | },
202 | onSongEnded(f) {
203 | $(".vocal").html("");
204 | }
205 | });
--------------------------------------------------------------------------------
/addons/accuracy_chart/script.js:
--------------------------------------------------------------------------------
1 | var prevBest = [];
2 | var currentAttempt = [];
3 | var betterTicks = [];
4 | var worseTicks = [];
5 | var myChart = null;
6 |
7 | var storage = new SnifferStorage("accuracy_chart");
8 |
9 | var poller = new SnifferPoller({
10 | onData: function(data) {
11 | if(poller.getCurrentState() == STATE_SONG_STARTING || poller.getCurrentState() == STATE_SONG_PLAYING || poller.getCurrentState() == STATE_SONG_ENDING) {
12 | setCurrentAttempt(poller.getSongTimer(), poller.getCurrentAccuracy());
13 | }
14 | },
15 | onSongStarted: function(song) {
16 | prevBest.length = 0;
17 | currentAttempt.length = 0;
18 | betterTicks.length = 0;
19 | worseTicks.length = 0;
20 |
21 | var arr_id = poller.getCurrentArrangement().arrangementID;
22 |
23 | storage.getValue(song.songID+"_"+arr_id).done(function(data) {
24 | var parsed = JSON.parse(data);
25 |
26 | if(parsed != null) {
27 | setPrevBest(parsed);
28 | }
29 | });
30 | },
31 | onSongEnded: function(song) {
32 | var arr_id = poller.getCurrentArrangement().arrangementID;
33 |
34 | if(prevBest.length <= 1) {
35 | storage.setValue(song.songID+"_"+arr_id, currentAttempt);
36 | console.log("Storing first attempt");
37 | return;
38 | }
39 |
40 | console.log("Current attempt: ",currentAttempt.slice(-1).pop().y);
41 | console.log("Best attempt: ",prevBest.slice(-1).pop().y);
42 |
43 | if(currentAttempt.slice(-1).pop().y > prevBest.slice(-1).pop().y) {
44 | storage.setValue(song.songID+"_"+arr_id, currentAttempt);
45 | console.log("Storing better attempt");
46 | }
47 | }
48 | });
49 |
50 | function setCurrentAttempt(time, accuracy) {
51 | var t = Math.floor(time);
52 |
53 | for (var i = 0; i < t; i++) {
54 | if(!currentAttempt[i]) {
55 | currentAttempt[i] = {x: i, y: accuracy};
56 | }
57 | if(!betterTicks[i]) {
58 | betterTicks[i] = null;
59 | }
60 | if(!worseTicks[i]) {
61 | worseTicks[i] = null;
62 | }
63 | }
64 |
65 | currentAttempt[t] = {x: t, y: accuracy}
66 |
67 | betterTicks[t] = null;
68 | worseTicks[t] = null;
69 |
70 | if(prevBest[t]) {
71 | if(currentAttempt[t].y >= prevBest[t].y) {
72 | betterTicks[t] = currentAttempt[t];
73 | } else {
74 | worseTicks[t] = currentAttempt[t];
75 | }
76 | }
77 |
78 | myChart.resetZoom();
79 | myChart.doPan(myChart.scales["x-axis-0"].getPixelForValue(-t), (myChart.height / 2) - myChart.scales["y-axis-0"].getPixelForValue(accuracy));
80 |
81 | myChart.doZoom(1.8, 1.5, {x: myChart.scales["x-axis-0"].getPixelForValue(t), y: myChart.scales["y-axis-0"].getPixelForValue(accuracy)});
82 | myChart.doPan((myChart.width*0.7), 0);
83 |
84 | myChart.update();
85 | }
86 |
87 | function setPrevBest(pb) {
88 | prevBest.length = 0;
89 |
90 | for (var i = 0; i < pb.length; i++) {
91 | prevBest.push(pb[i]);
92 | }
93 |
94 | myChart.update();
95 | }
96 |
97 | $(function() {
98 | var ctx = document.getElementById('acc_chart').getContext('2d');
99 | myChart = new Chart(ctx, {
100 | type: 'line',
101 | data: {
102 | datasets: [{
103 | label: "Previous Best",
104 | data: prevBest,
105 | backgroundColor: 'rgba(99, 132, 255, 0.4)',
106 | borderColor: 'rgba(255, 99, 132, 0)',
107 | borderWidth: 1,
108 | fill: "origin"
109 | },{
110 | label: "Current attempt",
111 | data: currentAttempt,
112 | borderColor: 'rgba(99, 132, 132, 1)',
113 | borderWidth: 1
114 | },
115 | {
116 | label: "Worse ticks",
117 | data: worseTicks,
118 | backgroundColor: 'rgba(255, 99, 132, 1)',
119 | fill: 0
120 | },
121 | {
122 | label: "Better ticks",
123 | data: betterTicks,
124 | backgroundColor: 'rgba(99, 255, 132, 1)',
125 | fill: 0
126 | }]
127 | },
128 | options: {
129 | animation: false,
130 | maintainAspectRatio: false,
131 | legend: {
132 | display: false
133 | },
134 | scales: {
135 | yAxes: [{
136 | ticks: {
137 | min: 0,
138 | max: 100,
139 | beginAtZero: true
140 | }
141 | }],
142 | xAxes: [{
143 | type: "linear",
144 | ticks: {
145 | min: 0,
146 | minRotation: 0,
147 | maxRotation: 0,
148 | callback: function(value, index, values) {
149 | var minutes = Math.floor(value/60);
150 | var seconds = value % 60;
151 |
152 | if(value < 0) {
153 | return "";
154 | }
155 |
156 | return [minutes,seconds].map(X => ('0' + Math.floor(X)).slice(-2)).join(':')
157 | }
158 | }
159 | }]
160 | },
161 | elements: {
162 | line: {
163 | tension: 0
164 | },
165 | point: {
166 | radius: 0
167 | }
168 | },
169 | plugins: {
170 | zoom: {
171 | pan: {
172 | enabled: true,
173 | mode: 'xy',
174 | speed: 10,
175 | threshold: 10
176 | },
177 | zoom: {
178 | enabled: true,
179 | mode: 'xy'
180 | }
181 | },
182 | }
183 | },
184 | plugins: [
185 | {
186 | afterDatasetsDraw: function(chartInstance) {
187 | var ctx = chartInstance.chart.ctx;
188 | var chartArea = chartInstance.chartArea;
189 |
190 | if(currentAttempt.length == 0) {
191 | return;
192 | }
193 |
194 | ctx.save();
195 |
196 | var pt = currentAttempt.slice(-1)[0];
197 | var x = chartInstance.scales["x-axis-0"].getPixelForValue(pt.x);
198 | var y = chartInstance.scales["y-axis-0"].getPixelForValue(pt.y);
199 |
200 | var text = pt.y+"%";
201 | var textColor = "black";
202 | var fontSize = 30;
203 |
204 | if(prevBest.length > 0) {
205 | var bpt = prevBest[pt.x];
206 |
207 | if(bpt.y > pt.y) {
208 | textColor = "red";
209 | text = (pt.y-bpt.y).toFixed(2)+"%";
210 | } else {
211 | textColor = "green";
212 | text = "+"+(pt.y-bpt.y).toFixed(2)+"%";
213 | }
214 |
215 | }
216 |
217 | ctx.font = fontSize+"px Arial";
218 | var textSize = ctx.measureText(text);
219 |
220 | var arrangement = poller.getCurrentArrangement();
221 |
222 | if(arrangement) {
223 | for (var i = 0; i < arrangement.sections.length; i++) {
224 | var section = arrangement.sections[i];
225 |
226 | var start = chartInstance.scales["x-axis-0"].getPixelForValue(section.startTime);
227 | var end = chartInstance.scales["x-axis-0"].getPixelForValue(section.endTime);
228 |
229 | ctx.fillStyle = "black";
230 | ctx.fillText(section.name, start, 30);
231 |
232 | ctx.moveTo(start, 0);
233 | ctx.lineTo(start, chartInstance.height);
234 | ctx.stroke();
235 |
236 | ctx.moveTo(end, 0);
237 | ctx.lineTo(end, chartInstance.height);
238 | ctx.stroke();
239 | }
240 | }
241 |
242 | ctx.translate(10,-fontSize/2)
243 |
244 | ctx.fillStyle = textColor;
245 | ctx.fillText(text, x, y+fontSize-4);
246 |
247 | ctx.restore();
248 | }
249 | }
250 | ]
251 | });
252 | });
--------------------------------------------------------------------------------
/addons/current_song_v4/script.js:
--------------------------------------------------------------------------------
1 |
2 | // OBS Configuration
3 | // Weight: 440, Height: 600
4 | // Custom CSS:
5 | // body { background-color: rgba(0, 0, 0, 0); margin: 6px 6px 6px 6px; overflow: hidden; }
6 |
7 | var poller = new SnifferPoller({
8 | interval: 500,
9 |
10 | onData: function(data) {
11 | app.snifferData = data;
12 | },
13 | onSongStarted: function(data) {
14 | app.mode = 0;
15 | app.visible = true;
16 | clearTimeout(hideTimeout);
17 | },
18 | onSongEnded: function(data) {
19 | app.prevData = app.snifferData;
20 | app.mode = 1;
21 |
22 | generateFeedback();
23 | }
24 | });
25 |
26 | var tracker = new PlaythroughTracker(poller);
27 |
28 | var app = new Vue({
29 | el: "#app",
30 | data: {
31 | visible: false,
32 | mode: 0,
33 | prevData: {},
34 | snifferData: {},
35 | feedback: [],
36 | feedbackIdx: 0
37 | },
38 | methods: {
39 | cycleFeedback: function() {
40 | if(this.mode == 1) {
41 | setTimeout(() => this.cycleFeedback(), 5000);
42 | this.feedbackIdx = (this.feedbackIdx+1) % this.feedback.length;
43 | }
44 | },
45 | hasPreviousBest: function() {
46 | return tracker.hasPreviousBest();
47 | },
48 | trackerScore: function() {
49 | return tracker.getFinal();
50 | }
51 | },
52 | computed: {
53 | song: function() {
54 | if(!this.snifferData) {
55 | return null;
56 | }
57 |
58 | return this.snifferData.songDetails;
59 | },
60 | readout: function() {
61 | if(!this.snifferData) {
62 | return null;
63 | }
64 |
65 | return this.snifferData.memoryReadout;
66 | },
67 | notes: function() {
68 | if(!this.snifferData) {
69 | return null;
70 | }
71 |
72 | return this.readout.noteData;
73 | },
74 | songLength: function() {
75 | return formatTimer(this.song.songLength);
76 | },
77 | songTimer: function() {
78 | return formatTimer(this.readout.songTimer);
79 | },
80 | songProgress: function() {
81 | return (this.readout.songTimer / this.song.songLength) * 100;
82 | },
83 | arrangement: function() {
84 | if(this.song == null) {return null;}
85 | if(this.song.arrangements == null) {return null;}
86 |
87 | for (var i = this.song.arrangements.length - 1; i >= 0; i--) {
88 | var arrangement = this.song.arrangements[i];
89 |
90 | if(arrangement.arrangementID == this.readout.arrangementID) {
91 | return arrangement;
92 | }
93 | }
94 |
95 | return null;
96 | },
97 | sections: function() {
98 | var arrangement = this.arrangement;
99 |
100 | if(arrangement == null) {return null;}
101 |
102 | var sections = arrangement.sections;
103 |
104 | var songLength = this.song.songLength;
105 |
106 | for (var i = 0; i < sections.length; i++) {
107 | var section = sections[i];
108 |
109 | if(this.readout.songTimer < section.endTime) {
110 | break;
111 | }
112 |
113 | section.length = section.endTime - section.startTime;
114 |
115 | section.startPercent = (section.startTime / songLength) * 100;
116 |
117 | //Always make the first section start from 0%
118 | if(i == 0) {
119 | section.length = section.endTime;
120 | section.startPercent = 0;
121 | }
122 |
123 | section.endPercent = (section.endTime / songLength) * 100;
124 |
125 | section.lengthPercent = (section.length / songLength) * 100;
126 |
127 | section.style = {
128 | left: section.startPercent+'%',
129 | width: section.lengthPercent+'%',
130 | backgroundColor: 'transparent'
131 | }
132 |
133 | sections[i] = section;
134 | }
135 |
136 | return sections;
137 | },
138 | /* PREV */
139 | prevSong: function() {
140 | return this.prevData.songDetails;
141 | },
142 | prevReadout: function() {
143 | if(!this.prevData) {
144 | return null;
145 | }
146 |
147 | return this.prevData.memoryReadout;
148 | },
149 | prevNotes: function() {
150 | if(!this.snifferData) {
151 | return null;
152 | }
153 |
154 | return this.prevReadout.noteData;
155 | },
156 | prevArrangement: function() {
157 | if(this.prevSong == null) {return null;}
158 | if(this.prevSong.arrangements == null) {return null;}
159 |
160 | for (var i = this.prevSong.arrangements.length - 1; i >= 0; i--) {
161 | var arrangement = this.prevSong.arrangements[i];
162 |
163 | if(arrangement.arrangementID == this.prevReadout.arrangementID) {
164 | return arrangement;
165 | }
166 | }
167 |
168 | return null;
169 | },
170 | prevSections: function() {
171 | var arrangement = this.prevArrangement;
172 |
173 | if(arrangement == null) {return null;}
174 |
175 | var sections = arrangement.sections;
176 |
177 | var songLength = this.prevSong.songLength;
178 |
179 | for (var i = 0; i < sections.length; i++) {
180 | var section = sections[i];
181 |
182 | section.length = section.endTime - section.startTime;
183 |
184 | section.startPercent = (section.startTime / songLength) * 100;
185 |
186 | //Always make the first section start from 0%
187 | if(i == 0) {
188 | section.length = section.endTime;
189 | section.startPercent = 0;
190 | }
191 |
192 | section.endPercent = (section.endTime / songLength) * 100;
193 |
194 | section.lengthPercent = (section.length / songLength) * 100;
195 |
196 | section.style = {
197 | left: section.startPercent+'%',
198 | width: section.lengthPercent+'%',
199 | backgroundColor: 'white'
200 | }
201 |
202 | sections[i] = section;
203 | }
204 |
205 | return sections;
206 | }
207 | }
208 | });
209 |
210 | function formatTimer(time) {
211 | var minutes = Math.floor(time/60);
212 | var seconds = time % 60;
213 |
214 | if(time < 0) {
215 | return "";
216 | }
217 |
218 | return [minutes,seconds].map(X => ('0' + Math.floor(X)).slice(-2)).join(':')
219 | }
220 |
221 | var hideTimeout = null;
222 | function generateFeedback() {
223 | app.feedback = [];
224 |
225 | var arrangement = poller.getCurrentArrangement();
226 | var sections = arrangement.sections;
227 | var feedback = []
228 |
229 | var greens = 0;
230 |
231 | for (var i = sections.length - 1; i >= 0; i--) {
232 | var section = sections[i];
233 | var rel = tracker.getRelative(section.endTime);
234 |
235 | if(rel == null) {
236 | continue;
237 | }
238 |
239 | if(rel.Accuracy >= 0) {
240 | greens++;
241 | }
242 |
243 | if(rel.Accuracy >= 1) {
244 | feedback.push("got "+rel.Accuracy.toFixed(2)+"% better accuracy in "+section.name);
245 | }
246 | if(rel.TotalNotesHit > 2) {
247 | feedback.push("hit "+rel.TotalNotesHit+" more notes in "+section.name);
248 | }
249 | }
250 |
251 | if(greens > 0) {
252 | feedback.push(greens+" green sections");
253 | }
254 |
255 | feedback.sort(() => Math.random() - 0.5);
256 |
257 | if(poller.getCurrentAccuracy() == 100) {
258 | feedback.push("hit all the notes");
259 | }
260 |
261 | if(feedback.length == 0) {
262 | feedback.push("you tried!");
263 | }
264 |
265 | app.feedback = feedback;
266 |
267 | app.cycleFeedback();
268 |
269 | hideTimeout = setTimeout(() => {if(app.mode == 1) {app.mode = 0; app.visible = false;}}, 60000);
270 | }
--------------------------------------------------------------------------------
/addons/current_song_v2/script.js:
--------------------------------------------------------------------------------
1 | var poller = new SnifferPoller({
2 | interval: 500,
3 |
4 | onData: function(data) {
5 | app.snifferData = data;
6 | },
7 | onSongStarted: function(data) {
8 | app.mode = 0;
9 | app.visible = true;
10 | clearTimeout(hideTimeout);
11 | },
12 | onSongEnded: function(data) {
13 | app.prevData = app.snifferData;
14 | app.mode = 1;
15 |
16 | generateFeedback();
17 | }
18 | });
19 |
20 | var tracker = new PlaythroughTracker(poller);
21 |
22 | var app = new Vue({
23 | el: "#app",
24 | data: {
25 | visible: false,
26 | mode: 0,
27 | prevData: {},
28 | snifferData: {},
29 | feedback: [],
30 | feedbackIdx: 0
31 | },
32 | methods: {
33 | cycleFeedback: function() {
34 | if(this.mode == 1) {
35 | setTimeout(() => this.cycleFeedback(), 5000);
36 | this.feedbackIdx = (this.feedbackIdx+1) % this.feedback.length;
37 | }
38 | },
39 | hasPreviousBest: function() {
40 | return tracker.hasPreviousBest();
41 | },
42 | trackerScore: function() {
43 | return tracker.getFinal();
44 | }
45 | },
46 | computed: {
47 | song: function() {
48 | if(!this.snifferData) {
49 | return null;
50 | }
51 |
52 | return this.snifferData.songDetails;
53 | },
54 | readout: function() {
55 | if(!this.snifferData) {
56 | return null;
57 | }
58 |
59 | return this.snifferData.memoryReadout;
60 | },
61 | notes: function() {
62 | if(!this.snifferData) {
63 | return null;
64 | }
65 |
66 | return this.readout.noteData;
67 | },
68 | songLength: function() {
69 | return formatTimer(this.song.songLength);
70 | },
71 | songTimer: function() {
72 | return formatTimer(this.readout.songTimer);
73 | },
74 | songProgress: function() {
75 | return (this.readout.songTimer / this.song.songLength) * 100;
76 | },
77 | arrangement: function() {
78 | if(this.song == null) {return null;}
79 | if(this.song.arrangements == null) {return null;}
80 |
81 | for (var i = this.song.arrangements.length - 1; i >= 0; i--) {
82 | var arrangement = this.song.arrangements[i];
83 |
84 | if(arrangement.arrangementID == this.readout.arrangementID) {
85 | return arrangement;
86 | }
87 | }
88 |
89 | return null;
90 | },
91 | sections: function() {
92 | var arrangement = this.arrangement;
93 |
94 | if(arrangement == null) {return null;}
95 |
96 | var sections = arrangement.sections;
97 |
98 | var songLength = this.song.songLength;
99 |
100 | for (var i = 0; i < sections.length; i++) {
101 | var section = sections[i];
102 |
103 | if(this.readout.songTimer < section.endTime) {
104 | break;
105 | }
106 |
107 | section.length = section.endTime - section.startTime;
108 |
109 | section.startPercent = (section.startTime / songLength) * 100;
110 |
111 | //Always make the first section start from 0%
112 | if(i == 0) {
113 | section.length = section.endTime;
114 | section.startPercent = 0;
115 | }
116 |
117 | section.endPercent = (section.endTime / songLength) * 100;
118 |
119 | section.lengthPercent = (section.length / songLength) * 100;
120 |
121 | section.style = {
122 | left: section.startPercent+'%',
123 | width: section.lengthPercent+'%',
124 | backgroundColor: 'transparent'
125 | }
126 |
127 | if(tracker.hasPreviousBest()) {
128 | section.style.backgroundColor = (tracker.isBetterRelative(section.endTime) ? "green" : "red");
129 | }
130 |
131 | sections[i] = section;
132 | }
133 |
134 | return sections;
135 | },
136 | /* PREV */
137 | prevSong: function() {
138 | return this.prevData.songDetails;
139 | },
140 | prevReadout: function() {
141 | if(!this.prevData) {
142 | return null;
143 | }
144 |
145 | return this.prevData.memoryReadout;
146 | },
147 | prevNotes: function() {
148 | if(!this.snifferData) {
149 | return null;
150 | }
151 |
152 | return this.prevReadout.noteData;
153 | },
154 | prevArrangement: function() {
155 | if(this.prevSong == null) {return null;}
156 | if(this.prevSong.arrangements == null) {return null;}
157 |
158 | for (var i = this.prevSong.arrangements.length - 1; i >= 0; i--) {
159 | var arrangement = this.prevSong.arrangements[i];
160 |
161 | if(arrangement.arrangementID == this.prevReadout.arrangementID) {
162 | return arrangement;
163 | }
164 | }
165 |
166 | return null;
167 | },
168 | prevSections: function() {
169 | var arrangement = this.prevArrangement;
170 |
171 | if(arrangement == null) {return null;}
172 |
173 | var sections = arrangement.sections;
174 |
175 | var songLength = this.prevSong.songLength;
176 |
177 | for (var i = 0; i < sections.length; i++) {
178 | var section = sections[i];
179 |
180 | section.length = section.endTime - section.startTime;
181 |
182 | section.startPercent = (section.startTime / songLength) * 100;
183 |
184 | //Always make the first section start from 0%
185 | if(i == 0) {
186 | section.length = section.endTime;
187 | section.startPercent = 0;
188 | }
189 |
190 | section.endPercent = (section.endTime / songLength) * 100;
191 |
192 | section.lengthPercent = (section.length / songLength) * 100;
193 |
194 | section.style = {
195 | left: section.startPercent+'%',
196 | width: section.lengthPercent+'%',
197 | backgroundColor: 'white'
198 | }
199 |
200 | if(tracker.hasPreviousBest()) {
201 | section.style.backgroundColor = (tracker.isBetterRelative(section.endTime) ? "green" : "red");
202 | }
203 |
204 | sections[i] = section;
205 | }
206 |
207 | return sections;
208 | }
209 | }
210 | });
211 |
212 | function formatTimer(time) {
213 | var minutes = Math.floor(time/60);
214 | var seconds = time % 60;
215 |
216 | if(time < 0) {
217 | return "";
218 | }
219 |
220 | return [minutes,seconds].map(X => ('0' + Math.floor(X)).slice(-2)).join(':')
221 | }
222 |
223 | var hideTimeout = null;
224 | function generateFeedback() {
225 | app.feedback = [];
226 |
227 | var arrangement = poller.getCurrentArrangement();
228 | var sections = arrangement.sections;
229 | var feedback = []
230 |
231 | var greens = 0;
232 |
233 | for (var i = sections.length - 1; i >= 0; i--) {
234 | var section = sections[i];
235 | var rel = tracker.getRelative(section.endTime);
236 |
237 | if(rel == null) {
238 | continue;
239 | }
240 |
241 | if(rel.Accuracy >= 0) {
242 | greens++;
243 | }
244 |
245 | if(rel.Accuracy >= 1) {
246 | feedback.push("got "+rel.Accuracy.toFixed(2)+"% better accuracy in "+section.name);
247 | }
248 | if(rel.TotalNotesHit > 2) {
249 | feedback.push("hit "+rel.TotalNotesHit+" more notes in "+section.name);
250 | }
251 | }
252 |
253 | if(greens > 0) {
254 | feedback.push(greens+" green sections");
255 | }
256 |
257 | feedback.sort(() => Math.random() - 0.5);
258 |
259 | if(poller.getCurrentAccuracy() == 100) {
260 | feedback.push("hit all the notes");
261 | }
262 |
263 | if(feedback.length == 0) {
264 | feedback.push("you tried!");
265 | }
266 |
267 | app.feedback = feedback;
268 |
269 | app.cycleFeedback();
270 |
271 | hideTimeout = setTimeout(() => {if(app.mode == 1) {app.mode = 0; app.visible = false;}}, 60000);
272 | }
273 |
--------------------------------------------------------------------------------
/addons/current_song/script.js:
--------------------------------------------------------------------------------
1 | //If you want to preview what the popup looks like, set this to true
2 | var preview = false;
3 |
4 | //How often to poll the addon service (in milliseconds)
5 | var pollrate = 900;
6 |
7 | //Should the popup be always visible or fade out in between songs
8 | var always_visible = false;
9 |
10 | //Enable accuracy percentage
11 | var show_accuracy = true;
12 |
13 | //Enable animation (percentage scrolls to the next number instead of snapping)
14 | var animate_percentage = true;
15 |
16 | //Enable colour animation (percentage fades from one color to another across 0%->100%)
17 | var animate_percentage_color = true;
18 |
19 | //Enable a media player style progress bar on the bottom
20 | var show_progress = true;
21 |
22 | //Color for 0%
23 | var color_0 = "#FF0000";
24 |
25 | //Color for 50%
26 | var color_50 = "#FFFF00";
27 |
28 | //Color for 100%
29 | var color_100 = "#00FF00";
30 |
31 | //The point at which the 50% color is placed in the gradient, number between 0 and 1
32 | var color_midpoint = 0.8;
33 |
34 | //Exponent for interpolation, higher number = steeper curve towards the end, 1 = linear interpolation
35 | var color_exponent = 2;
36 |
37 | //Extend jQuery
38 | jQuery.fn.extend({
39 | textStroke(text) {
40 | return setTextStroke(this, text);
41 | }
42 | });
43 |
44 | //JQuerys document.onReady function
45 | //Gets called after the webpage is loaded
46 | $(function() {
47 | //If always visible, show the popup
48 | if(always_visible) {showPopup()};
49 |
50 | //Hide progress if disabled
51 | if(!show_accuracy) {
52 | $("h1.accuracy_percentage").hide();
53 | }
54 |
55 | //Hide progress bar if disabled
56 | if(!show_progress) {
57 | $("div.progress_bar").hide();
58 | }
59 |
60 | $(".stroke").each(function() {
61 | setTextStroke($(this), $(this).text());
62 | });
63 |
64 | //If preview is enabled, show the popup with some example text
65 | if(preview) {
66 | $("h1.artist_name").textStroke("Artist name");
67 | $("h1.song_name").textStroke("Song name");
68 | $("h1.album_name").textStroke("Album name (1234)");
69 | $("h1.accuracy_percentage").textStroke("100%");
70 | $("img.album_cover").attr("src", "rs_pick.png");
71 | showPopup();
72 |
73 | if(animate_percentage) {
74 | testAnim = function() {
75 | $("h1.accuracy_percentage").finish().animateNumber({
76 | number: 100,
77 | numberStep: function(now, tween) {
78 | $(tween.elem).textStroke(now.toFixed(2)+"%");
79 |
80 | if(animate_percentage_color) {
81 | $(tween.elem).css("color", lerpColors(now/100));
82 | }
83 | }
84 | }, 5000);
85 | };
86 |
87 | testAnim();
88 |
89 | setInterval(testAnim, 6000);
90 | }
91 |
92 | testProgressBar = function() {
93 | $("div.progress_bar_inner").finish().animateNumber({
94 | number: 600,
95 | numberStep: function(now, tween) {
96 | $(tween.elem).css("width",100*(now/600)+"%");
97 | $(tween.elem).prev().textStroke(durationString(now)+"/"+durationString(600));
98 | }
99 | }, 5000);
100 | }
101 |
102 | testProgressBar();
103 |
104 | setInterval(testProgressBar, 6000);
105 | }
106 | else {
107 | //Set a timer to refresh our data
108 | setInterval(refresh, pollrate);
109 | refresh();
110 | }
111 | });
112 |
113 | //Remember popup visibility
114 | var visible = false;
115 |
116 | //Remember previous percentage for the animation
117 | var prev_accuracy = 0;
118 |
119 | function refresh() {
120 | //JSON query the addon service
121 | $.getJSON("http://"+ip+":"+port, function(data) {
122 | //If data was successfully gotten
123 | if(data.success) {
124 | //Get song details out of it
125 | var details = data.songDetails;
126 |
127 | //Get memory readout
128 | var readout = data.memoryReadout;
129 |
130 | //If song details are invalid, hide popup
131 | if(details.songLength == 0 && details.albumYear == 0 && details.numArrangements == 0) {
132 | hidePopup();
133 | return;
134 | }
135 |
136 | //Transfer data onto DOM elements using JQuery selectors
137 | $("h1.artist_name").textStroke(details.artistName);
138 | $("h1.song_name").textStroke(details.songName);
139 | $("h1.album_name").textStroke(details.albumName + " (" + details.albumYear + ")");
140 |
141 | //Get accuracy
142 | var accuracy = readout.noteData.Accuracy
143 |
144 | if(animate_percentage) {
145 | //Format it and apply it to the element
146 | $("h1.accuracy_percentage").prop("number", prev_accuracy).finish().animateNumber({
147 | number: accuracy,
148 | numberStep: function(now, tween) {
149 | $(tween.elem).textStroke(now.toFixed(2)+"%");
150 |
151 | if(animate_percentage_color) {
152 | $(tween.elem).css("color", lerpColors(now/100));
153 | }
154 | }
155 | }, pollrate);
156 |
157 | //Remember previous accuracy
158 | prev_accuracy = accuracy;
159 | } else {
160 | $("h1.accuracy_percentage").textStroke(accuracy.toFixed(2)+"%");
161 | }
162 |
163 | //Update progress bar
164 | $("div.progress_bar_inner").css("width",100*(readout.songTimer/details.songLength)+"%");
165 | $("p.progress_bar_text").text(durationString(readout.songTimer)+"/"+durationString(details.songLength));
166 |
167 | //Set the album art, which is base64 encoded, HTML can handle that, just append
168 | //"data:image/jpeg; base64, " in front to tell HTML how to use the data
169 | if(details.albumArt != null) {
170 | $("img.album_cover").attr("src", "data:image/jpeg;base64, " + details.albumArt);
171 | }
172 |
173 | //If the song timer is over 1 second, we are playing a song, so show the popup
174 | if(readout.songTimer > 1) {
175 | showPopup();
176 | }else if(readout.songTimer <= 1) { //Else if the song timer is less than a second, we probably aren't playing anything
177 | hidePopup();
178 | }
179 | }else { //If the data getting was not successful, hide the popup
180 | hidePopup();
181 | }
182 | }, "json");
183 | }
184 |
185 | //Function to set text of an element and update the stoke attribute
186 | function setTextStroke(elem, text) {
187 | return elem.text(text).addClass("stroke").attr("data-stroke", text);
188 | }
189 |
190 | //Hides the popup if it is visible
191 | function hidePopup() {
192 | if(always_visible) return;
193 | if(!visible) return;
194 |
195 | //Do a fadeout over 1000 milliseconds and set the visible variable to false
196 | $("div.popup").fadeOut(1000);
197 | visible = false;
198 | }
199 |
200 | //Shows the popup if it is hidden
201 | function showPopup() {
202 | if(visible) return;
203 |
204 | //Do a fadein over 1000 milliseconds and set the visible variable to true
205 | $("div.popup").fadeIn(1000);
206 | visible = true;
207 | }
208 |
209 | //Convert a number to a duration "hh:mm:ss"
210 | function durationString(tSeconds) {
211 | var hh = Math.floor(tSeconds / 3600);
212 | var mm = Math.floor((tSeconds - (hh * 3600)) / 60);
213 | var ss = Math.floor(tSeconds % 60);
214 |
215 | if(hh < 10) {hh = "0"+hh;}
216 | if(mm < 10) {mm = "0"+mm;}
217 | if(ss < 10) {ss = "0"+ss;}
218 |
219 | if(hh > 0) {
220 | return hh+":"+mm+":"+ss;
221 | } else {
222 | return mm+":"+ss;
223 | }
224 | }
225 |
226 | //Lerp between the 0%, 50% and 100% colors
227 | function lerpColors(p) {
228 | //Transform to logarithmic scale
229 | p = Math.pow(p, color_exponent)
230 |
231 | if(p <= color_midpoint) {
232 | return lerpColor(color_0, color_50, p / color_midpoint);
233 | } else {
234 | return lerpColor(color_50, color_100, (p - color_midpoint) / (1 - color_midpoint));
235 | }
236 | }
237 |
238 | /** https://gist.github.com/rosszurowski/67f04465c424a9bc0dae
239 | * A linear interpolator for hexadecimal colors
240 | * @param {String} a
241 | * @param {String} b
242 | * @param {Number} amount
243 | * @example
244 | * // returns #7F7F7F
245 | * lerpColor('#000000', '#ffffff', 0.5)
246 | * @returns {String}
247 | */
248 | function lerpColor(a, b, amount) {
249 | //Clamp the amount to 0-1 range
250 | amount = Math.min(1, Math.max(0,amount));
251 |
252 | var ah = parseInt(a.replace(/#/g, ''), 16),
253 | ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff,
254 | bh = parseInt(b.replace(/#/g, ''), 16),
255 | br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff,
256 | rr = ar + amount * (br - ar),
257 | rg = ag + amount * (bg - ag),
258 | rb = ab + amount * (bb - ab);
259 |
260 | return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1);
261 | }
--------------------------------------------------------------------------------
/addons/_deps/sniffer-poller.js:
--------------------------------------------------------------------------------
1 | const STATE_NONE = 0;
2 | const STATE_IN_MENUS = 1;
3 | const STATE_SONG_SELECTED = 2;
4 | const STATE_SONG_STARTING = 3;
5 | const STATE_SONG_PLAYING = 4;
6 | const STATE_SONG_ENDING = 5;
7 |
8 | class SnifferPoller {
9 | //Create variables and containers for poller data.
10 | constructor(options = {}) {
11 | var defaultOptions = {
12 | ip: ip,
13 | port: port,
14 | interval: 900,
15 |
16 | onData: (data) => {},
17 | onSongChanged: (songData) => {console.log("onSongChanged",songData)},
18 | onSongStarted: (songData) => {console.log("onSongStarted",songData)},
19 | onSongEnded: (songData) => {console.log("onSongEnded",songData)},
20 | onStateChanged: (oldState, newState) => {console.log("onStateChanged",oldState+"=>"+newState)}
21 | }
22 |
23 | //Set up options
24 | this.options = {}
25 | $.extend(this.options, defaultOptions, options);
26 |
27 | //Poll interval
28 | this.polltimer = setInterval(() => this.poll(), this.options.interval);
29 |
30 | //Some internal state variables
31 | this.songStarted = false;
32 |
33 | this.callbacks = {
34 | onData: [],
35 | onSongChanged: [],
36 | onSongStarted: [],
37 | onSongEnded: [],
38 | onStateChanged: []
39 | }
40 | }
41 |
42 | //Trigger for onData
43 | onData(f) {
44 | this.callbacks.onData.push(f);
45 | }
46 |
47 | //Event triggers for onData
48 | _doOnData(data) {
49 | this.options.onData(data);
50 |
51 | for (var i = this.callbacks.onData.length - 1; i >= 0; i--) {
52 | this.callbacks.onData[i](data);
53 | }
54 | }
55 |
56 | //Trigger for song change
57 | onSongChanged(f) {
58 | this.callbacks.onSongChanged.push(f);
59 | }
60 |
61 | //Event triggers for song change
62 | _doOnSongChanged(song) {
63 | this.options.onSongChanged(song);
64 |
65 | for (var i = this.callbacks.onSongChanged.length - 1; i >= 0; i--) {
66 | this.callbacks.onSongChanged[i](song);
67 | }
68 | }
69 |
70 | //Trigger for song start
71 | onSongStarted(f) {
72 | this.callbacks.onSongStarted.push(f);
73 | }
74 |
75 | //Event triggers for song start
76 | _doOnSongStarted(song) {
77 | this.options.onSongStarted(song);
78 |
79 | for (var i = this.callbacks.onSongStarted.length - 1; i >= 0; i--) {
80 | this.callbacks.onSongStarted[i](song);
81 | }
82 | }
83 |
84 | //Event triggers for song end
85 | onSongEnded(f) {
86 | this.callbacks.onSongEnded.push(f);
87 | }
88 |
89 | //Event triggers for song end
90 | _doOnSongEnded(song) {
91 | this.options.onSongEnded(song);
92 |
93 | for (var i = this.callbacks.onSongEnded.length - 1; i >= 0; i--) {
94 | this.callbacks.onSongEnded[i](song);
95 | }
96 | }
97 |
98 | //Set up event triggers
99 | onStateChanged(f) {
100 | this.callbacks.onStateChanged.push(f);
101 | }
102 |
103 | //Event triggers when game state changes
104 | _doOnStateChanged(oldState, newState) {
105 | this.options.onStateChanged(oldState, newState);
106 |
107 | for (var i = this.callbacks.onStateChanged.length - 1; i >= 0; i--) {
108 | this.callbacks.onStateChanged[i](oldState, newState);
109 | }
110 | }
111 |
112 | //Set event triggers when data
113 | gotData(data) {
114 | //If we have no previous data, fire all events
115 | if(!this._prevdata) {
116 | this._doOnStateChanged(STATE_NONE, data.currentState);
117 | this._doOnSongChanged(data.songDetails);
118 |
119 | this._prevdata = data;
120 | this._doOnData(data);
121 |
122 | return;
123 | }
124 |
125 | if(this._prevdata.currentState != data.currentState) {
126 | this._doOnStateChanged(this._prevdata.currentState, data.currentState);
127 |
128 | if(this._prevdata.currentState == STATE_SONG_ENDING && data.currentState == STATE_IN_MENUS) {
129 | this._doOnSongEnded(data.songDetails);
130 | }
131 |
132 | if(data.currentState == STATE_IN_MENUS) {
133 | this.songStarted = false;
134 | }
135 | }
136 |
137 | if(this._prevdata.songDetails && data.songDetails && this._prevdata.songDetails.songID != data.songDetails.songID) {
138 | this._doOnSongChanged(data.songDetails);
139 | }
140 |
141 | //Don't fire song started before we have a valid arrangement
142 | if(!this.songStarted) {
143 | if(data.currentState == STATE_SONG_STARTING || data.currentState == STATE_SONG_PLAYING) {
144 | if(this.getCurrentArrangement() != null) {
145 | this.songStarted = true;
146 | this._doOnSongStarted(data.songDetails);
147 | }
148 | }
149 | }
150 |
151 |
152 | this._prevdata = data;
153 | this._doOnData(data);
154 | }
155 |
156 | //Get current data
157 | getCurrentReadout() {
158 | return this._prevdata.memoryReadout;
159 | }
160 |
161 | //Get current song
162 | getCurrentSong() {
163 | return this._prevdata.songDetails;
164 | }
165 |
166 | //Get current game state
167 | getCurrentState() {
168 | return this._prevdata.currentState;
169 | }
170 |
171 | //Get song timer
172 | getSongTimer() {
173 | return this._prevdata.memoryReadout.songTimer;
174 | }
175 |
176 | //Get current accuract
177 | getCurrentAccuracy(decimals = 2) {
178 | //Get accuracy
179 | var accuracy = this._prevdata.memoryReadout.noteData.Accuracy;
180 |
181 | //Round to decimals
182 | return parseFloat(accuracy.toFixed(decimals));
183 | }
184 |
185 | //Get current arrangement
186 | getCurrentArrangement() {
187 | if(!this._prevdata) {
188 | return null;
189 | }
190 |
191 | if(!this._prevdata.memoryReadout) {
192 | return null;
193 | }
194 |
195 | //Look through arrangements for matching ID
196 | for (var i = this._prevdata.songDetails.arrangements.length - 1; i >= 0; i--) {
197 | var arrangement = this._prevdata.songDetails.arrangements[i];
198 |
199 | //Check that ID is correctly formatted-32 characters long-to avoid errors
200 | if(arrangement.arrangementID.length == 32 && arrangement.arrangementID == this._prevdata.memoryReadout.arrangementID) {
201 | return arrangement;
202 | }
203 | }
204 |
205 | //If no matching ID, look for arragnement that matches previous path
206 | for (var i = this._prevdata.songDetails.arrangements.length - 1; i >= 0; i--) {
207 | var arrangement = this._prevdata.songDetails.arrangements[i];
208 |
209 | if(this.prevPath != null && arrangement.name == this.prevPath && arrangement.type == this.prevPath && arrangement.isBonusArrangement == false && arrangement.isAlternateArrangement == false) {
210 | return arrangement;
211 | }
212 | }
213 |
214 | //If no previous path, resort to default path listed at top
215 | for (var i = this._prevdata.songDetails.arrangements.length - 1; i >= 0; i--) {
216 | var arrangement = this._prevdata.songDetails.arrangements[i];
217 |
218 | if(arrangement.name == defaultPath && arrangement.type == defaultPath && arrangement.isBonusArrangement == false && arrangement.isAlternateArrangement == false) {
219 | return arrangement;
220 | }
221 | }
222 | }
223 |
224 | //Get section at current time
225 | getCurrentSection() {
226 | return this.getSectionAt(this.getSongTimer());
227 | }
228 |
229 | //Get section at specific time
230 | getSectionAt(time) {
231 | var arrangement = this.getCurrentArrangement();
232 |
233 | if(!arrangement) {
234 | return null;
235 | }
236 |
237 | for (var i = arrangement.sections.length-1; i >= 0; i--) {
238 | var section = arrangement.sections[i];
239 | section.index = i;
240 |
241 | if(time > section.startTime) {
242 | return section;
243 | }
244 | }
245 |
246 | return arrangement.sections[0];
247 | }
248 |
249 | //Get phrase at current time
250 | getCurrentPhrase() {
251 | return this.getPhraseAt(this.getSongTimer());
252 | }
253 |
254 | //Get phrase at specific time
255 | getPhraseAt(time) {
256 | var arrangement = this.getCurrentArrangement();
257 |
258 | if(!arrangement) {
259 | return null;
260 | }
261 |
262 | for (var i = arrangement.phraseIterations.length-1; i >= 0; i--) {
263 | var phrase = arrangement.phraseIterations[i];
264 | phrase.index = i;
265 |
266 | if(time > phrase.startTime) {
267 | return phrase;
268 | }
269 | }
270 |
271 | return arrangement.phraseIterations[0];
272 | }
273 |
274 | //get Maximum Difficulty from an arrangement
275 | getMaxDif(){
276 | var arrangement = this.getCurrentArrangement();
277 | if(!arrangement) {
278 | return 0;
279 | }
280 | var maxDif = 0;
281 | for (var i = 0; i < arrangement.phraseIterations.length; i++) {
282 | var phrase = arrangement.phraseIterations[i];
283 | if(phrase.maxDifficulty > maxDif){
284 | maxDif = phrase.maxDifficulty;
285 | }
286 | }
287 | return maxDif;
288 | }
289 |
290 | poll() {
291 | $.getJSON("http://"+this.options.ip+":"+this.options.port, (data) => this.gotData(data));
292 | }
293 |
294 | stop() {
295 | clearInterval(this.polltimer);
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/RockSniffer/RPC/DiscordRPCHandler.cs:
--------------------------------------------------------------------------------
1 | using DiscordRPC;
2 | using DiscordRPC.Logging;
3 | using RockSnifferLib.Logging;
4 | using RockSnifferLib.RSHelpers;
5 | using RockSnifferLib.RSHelpers.NoteData;
6 | using RockSnifferLib.Sniffing;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.Timers;
11 |
12 | namespace RockSniffer.RPC
13 | {
14 | public class DiscordRPCHandler : IDisposable
15 | {
16 | private DiscordRpcClient client;
17 | private RSMemoryReadout readout;
18 | private SnifferState state = SnifferState.NONE;
19 | private SongDetails songdetails;
20 | private AlbumArtResolver? albumArtResolver;
21 |
22 | private System.Timers.Timer timer;
23 | private readonly object membersLock = new object();
24 |
25 | private readonly Dictionary gcadeGames = new Dictionary()
26 | {
27 | ["GC_WhaleRider"] = "Gone Wailin'!",
28 | ["GC_StringSkipSaloon"] = "String Skip Saloon",
29 | ["GC_DucksPlus"] = "Ducks ReDux",
30 | ["GC_NinjaSlides"] = "Ninja Slide N",
31 | ["GC_ScaleWarriorsMenu"] = "Scale Warriors",
32 | ["GC_RailShooterMenu"] = "Return to Chastle Chordead",
33 | ["GC_TrackAndField"] = "Hurtlin' Hurdles",
34 | ["GC_TempleOfBends"] = "Temple of Bends",
35 | ["GC_ScaleRacer"] = "Scale Racer",
36 | ["GC_StarChords"] = "Star Chords",
37 | ["GC_HarmonicHeist"] = "Harmonic Heist"
38 | };
39 |
40 | private readonly DateTime appStartTime;
41 |
42 | public DiscordRPCHandler(Sniffer sniffer)
43 | {
44 | client = new DiscordRpcClient(Program.config.rpcSettings.client_id);
45 |
46 | if (Program.config.rpcSettings.enableCoverArt)
47 | {
48 | albumArtResolver = new AlbumArtResolver();
49 | }
50 |
51 | //Set the logger
52 | client.Logger = new ConsoleLogger() { Level = LogLevel.Warning };
53 |
54 | //Subscribe to events
55 | client.OnReady += (sender, e) =>
56 | {
57 | Logger.Log("[RPC] Received Ready from user {0}", e.User.Username);
58 | UpdatePresence();
59 | };
60 |
61 | //Initialize rpc client
62 | client.Initialize();
63 |
64 | appStartTime = DateTime.UtcNow;
65 |
66 | //Listen to events
67 | sniffer.OnStateChanged += Sniffer_OnStateChanged;
68 | sniffer.OnMemoryReadout += Sniffer_OnMemoryReadout;
69 | sniffer.OnSongChanged += Sniffer_OnSongChanged;
70 |
71 | // Set up presence update timer
72 | timer = new System.Timers.Timer(Math.Max(250, Program.config.rpcSettings.updatePeriodMs));
73 | timer.Elapsed += CondUpdatePresence;
74 | timer.AutoReset = true;
75 | timer.Enabled = true;
76 | }
77 |
78 | internal void CondUpdatePresence(Object source, ElapsedEventArgs e) {
79 | lock (membersLock)
80 | {
81 | UpdatePresence();
82 | }
83 | }
84 |
85 | internal void UpdatePresence()
86 | {
87 | //Construct rich presence
88 | var rp = new RichPresence();
89 | rp.Assets = new Assets();
90 | rp.Assets.LargeImageKey = "rocksmith";
91 | rp.Assets.LargeImageText = "Rocksmith 2014 Remastered";
92 |
93 | //If we have a valid song and are playing a song
94 | if ((songdetails != null && readout != null) && (state == SnifferState.SONG_STARTING || state == SnifferState.SONG_PLAYING || state == SnifferState.SONG_ENDING))
95 | {
96 | try
97 | {
98 | // Get the appropriate album cover
99 | if (albumArtResolver != null && albumArtResolver.Get(songdetails) is (string resURL, string resDisplayText) resultTuple)
100 | {
101 | rp.Assets.LargeImageKey = resURL;
102 | rp.Assets.LargeImageText = resDisplayText.Substring(0, Math.Min(resDisplayText.Length, 128));
103 | }
104 | }
105 | catch (Exception ex) { Logger.LogException(ex); }
106 |
107 | //Get the arrangement based on the arrangement id
108 | var arrangement = songdetails.arrangements.FirstOrDefault(x => x.arrangementID == readout.arrangementID);
109 |
110 | //Add song name
111 | rp.Details = $"Playing {songdetails.songName}";
112 |
113 | //Add artist name
114 | rp.State = $"by {songdetails.artistName}";
115 |
116 | //Set song timer
117 | rp.Timestamps = new Timestamps(DateTime.UtcNow.AddSeconds(-readout.songTimer), DateTime.UtcNow.AddSeconds(songdetails.songLength - readout.songTimer));
118 |
119 | //Calculate accuracy
120 | float accuracy = readout.noteData.Accuracy;
121 |
122 | string accuracyText = FormattableString.Invariant($"{accuracy:F2}%");
123 |
124 | //Set accuracy as text for arrangement icon
125 | rp.Assets.SmallImageText = accuracyText;
126 |
127 | if (readout.mode == RSMode.SCOREATTACK)
128 | {
129 | var sand = (ScoreAttackNoteData)readout.noteData;
130 |
131 | rp.Assets.SmallImageText = $"{FormattableString.Invariant($"{sand.CurrentScore:n0}")} x{sand.CurrentMultiplier} | {rp.Assets.SmallImageText}";
132 |
133 | if (sand.FailedPhrases > 0)
134 | {
135 | rp.Assets.SmallImageText = $"{new string('X', sand.FailedPhrases)} | {rp.Assets.SmallImageText}";
136 | }
137 | }
138 |
139 | //When we got the arrangement
140 | if (arrangement != null)
141 | {
142 | //Set arrangement icon
143 | rp.Assets.SmallImageKey = arrangement.type.ToLower();
144 |
145 | //Try to get section
146 | var section = arrangement.sections.LastOrDefault(x => x.startTime < readout.songTimer);
147 |
148 | //If we got a section
149 | if (section != null)
150 | {
151 | //Add section to small image text
152 | rp.Assets.SmallImageText = $"{section.name} | {rp.Assets.SmallImageText}";
153 | }
154 | }
155 |
156 | if (string.IsNullOrEmpty(rp.Assets.SmallImageKey) && rp.Assets.LargeImageKey != "rocksmith")
157 | rp.Assets.SmallImageKey = "rocksmith";
158 | }
159 | else
160 | {
161 | rp.Details = "Browsing Menus";
162 |
163 | if (readout != null)
164 | {
165 | string gameStage = readout.gameStage.ToLowerInvariant().Trim();
166 |
167 | string state = "";
168 |
169 | if (gameStage.StartsWith("main"))
170 | {
171 | state = "Main Menu";
172 | }
173 | else if (gameStage.StartsWith("las"))
174 | {
175 | state = "Learn A Song";
176 | }
177 | else if (gameStage.StartsWith("sm"))
178 | {
179 | state = "Session Mode";
180 | }
181 | else if (gameStage.StartsWith("nsp"))
182 | {
183 | state = "Nonstop Play";
184 | }
185 | else if (gameStage.StartsWith("sa"))
186 | {
187 | state = "Score Attack";
188 | }
189 | else if (gameStage.StartsWith("guitarcade") || gameStage.StartsWith("gc_games"))
190 | {
191 | state = "Guitarcade";
192 | }
193 | else if (gameStage.StartsWith("gcade"))
194 | {
195 | rp.Details = "Playing Guitarcade";
196 | state = "";
197 |
198 | if (gcadeGames.ContainsKey(readout.songID))
199 | {
200 | state = gcadeGames[readout.songID];
201 | }
202 | }
203 | else if (gameStage.StartsWith("ge_"))
204 | {
205 | state = "Lessons";
206 | }
207 | else if (gameStage.StartsWith("mp_"))
208 | {
209 | state = "Multiplayer";
210 | }
211 | else if (gameStage.StartsWith("shop"))
212 | {
213 | state = "Shop";
214 | }
215 |
216 |
217 | rp.State = state;
218 | }
219 |
220 | rp.Timestamps = new Timestamps(appStartTime);
221 | }
222 |
223 | client.SetPresence(rp);
224 | }
225 |
226 | private void Sniffer_OnSongChanged(object sender, RockSnifferLib.Events.OnSongChangedArgs e)
227 | {
228 | lock (membersLock) {
229 | songdetails = e.songDetails;
230 | }
231 | }
232 |
233 | private void Sniffer_OnMemoryReadout(object sender, RockSnifferLib.Events.OnMemoryReadoutArgs e)
234 | {
235 | lock (membersLock)
236 | {
237 | readout = e.memoryReadout;
238 | }
239 | }
240 |
241 | private void Sniffer_OnStateChanged(object sender, RockSnifferLib.Events.OnStateChangedArgs e)
242 | {
243 | lock (membersLock)
244 | {
245 | state = e.newState;
246 | }
247 | }
248 |
249 | public void Dispose()
250 | {
251 | Logger.Log("[RPC] Disposing");
252 | client.Dispose();
253 | }
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/addons/timeline/script.js:
--------------------------------------------------------------------------------
1 | //OBS websocket details
2 | var obs_ip = "localhost";
3 | var obs_port = 4444;
4 | var obs_password = "who needs a password anyway";
5 |
6 | var songID = "";
7 | var poller = new SnifferPoller({
8 | interval: 500,
9 |
10 | onData: function(data) {
11 | songDisplay.addonData = data;
12 |
13 | if(data.memoryReadout !== null) {
14 | timeline.setCustomTime(timeToDate(data.memoryReadout.songTimer));
15 | }
16 |
17 | if(data.songDetails !== null) {
18 | if(data.songDetails.songID != songID) {
19 | songID = data.songDetails.songID;
20 |
21 | loadTimeline();
22 |
23 | timeline.getOptions().max = timeToDate(data.songDetails.songLength);
24 | timeline.redraw();
25 | }
26 | }
27 |
28 | evalSequencer();
29 | }
30 | });
31 |
32 | var events = {
33 | "Scene": {
34 | desc: "Switch to scene",
35 | start: function(row) {
36 |
37 | },
38 | end: function(row) {
39 | obs.SetCurrentScene({"scene-name": row.sceneName});
40 | },
41 | getRow: function(options, editing) {
42 | return {
43 | content: "Switch to "+options.scene,
44 | sceneName: options.scene
45 | };
46 | },
47 | options: {
48 | "scene": {
49 | name: "Scene name",
50 | type: "select",
51 | options: function() {
52 | return obs_scenes.map(x => x.name).sort(ialphabetically);
53 | }
54 | }
55 | }
56 | },
57 | "Source": {
58 | desc: "Show source",
59 | start: function(row) {
60 | obs.SetSceneItemProperties({"scene-name": row.sceneName, "item": row.sourceName, "visible": true})
61 | },
62 | end: function(row) {
63 | obs.SetSceneItemProperties({"scene-name": row.sceneName, "item": row.sourceName, "visible": false})
64 | },
65 | getRow: function(options, editing) {
66 | var curRow = timeline.getItem(getSelectedRow());
67 |
68 | var newrow = {
69 | content: options.source+" in "+options.scene,
70 | sceneName: options.scene,
71 | sourceName: options.source
72 | };
73 |
74 | if(editing) {
75 | newrow.start = curRow.start;
76 | newrow.end = curRow.end;
77 | } else {
78 | newrow.end = new Date(curRow.start.getTime() + (10 * 1000));
79 | }
80 |
81 | return newrow;
82 | },
83 | options: {
84 | "scene": {
85 | name: "Scene name",
86 | type: "select",
87 | options: function(curOptions) {
88 | return obs_scenes.map(x => x.name).sort(ialphabetically);
89 | }
90 | },
91 | "source": {
92 | name: "Source name",
93 | type: "select",
94 | options: function(curOptions) {
95 | var curScene = obs_scenes.find(function(x) {
96 | return x.name == curOptions.scene;
97 | });
98 |
99 | if(typeof(curScene) !== "undefined") {
100 | return curScene.sources.map(x => x.name).sort(ialphabetically);
101 | }
102 |
103 | return null;
104 | }
105 | }
106 | }
107 | }
108 | };
109 |
110 | //JQuerys document.onReady function
111 | //Gets called after the webpage is loaded
112 | $(function() {
113 | $("div#eventDialog").dialog({
114 | title: "Add event",
115 | autoOpen: false,
116 | modal: true,
117 | buttons: {
118 | "Go": function() {
119 | var event = events[eventDialog.currentET];
120 |
121 | var opts = event.options;
122 |
123 | var builtops = {};
124 |
125 | for(var key in opts) {
126 | builtops[key] = $(this).find("#"+key).val();
127 | }
128 |
129 |
130 | //Hey, I know its ugly, but it works
131 | var row = event.getRow(builtops, $(this).dialog("option","title").toLowerCase().includes("edit"));
132 |
133 | row["event"] = eventDialog.currentET;
134 |
135 | updateSelectedRow(row);
136 |
137 | $(this).dialog("close");
138 | },
139 | "Cancel": function() {
140 | if(!$(this).dialog("option","title").toLowerCase().includes("edit")) {
141 | timeline.deleteItem(timeline.selection.index)
142 | }
143 |
144 | $(this).dialog("close");
145 | }
146 | }
147 | });
148 |
149 | obs = new OBSWebSocket();
150 | obs.connect({address: obs_ip+":"+obs_port, password:obs_password}).then(refreshOBS);
151 |
152 | storage = new SnifferStorage("timeline");
153 |
154 | songDisplay = new Vue({
155 | el: "#songDisplay",
156 | data: {
157 | addonData: null
158 | },
159 | computed: {
160 | status: function() {
161 | var status = "";
162 |
163 | if(obs._connected) {
164 | status += "Connected to OBS, ";
165 | }
166 | else {
167 | status += "Not connected to OBS, ";
168 | }
169 |
170 | if(this.addonData === null) {
171 | status += "Cannot connect to RockSniffer";
172 | }
173 | else {
174 | status += "Connected to RockSniffer " + this.addonData.Version;
175 | }
176 |
177 | return status;
178 | }
179 | }
180 | });
181 |
182 | eventDialog = new Vue({
183 | el: "form#eventDialog",
184 | data: {
185 | currentET: null,
186 | curOptions: {
187 |
188 | }
189 | },
190 | computed: {
191 | eventTypes: function() {
192 | return Object.keys(events);
193 | },
194 | curEvent: function() {
195 | return events[this.currentET];
196 | }
197 | }
198 | });
199 |
200 | timeline = new links.Timeline(document.getElementById("timeline"));
201 |
202 | timeline.step.getLabelMinor = function(options, date) {
203 | if (date == undefined) {
204 | date = this.current;
205 | }
206 |
207 | switch (this.scale) {
208 | case links.Timeline.StepDate.SCALE.MILLISECOND:
209 | return this.addZeros(date.getSeconds(), 2) + "." + String(date.getMilliseconds());
210 | case links.Timeline.StepDate.SCALE.SECOND:
211 | case links.Timeline.StepDate.SCALE.MINUTE:
212 | case links.Timeline.StepDate.SCALE.HOUR:
213 | if(date.getHours() > 0)
214 | return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2) + ":" + this.addZeros(date.getSeconds(), 2);
215 | return this.addZeros(date.getMinutes(), 2) + ":" + this.addZeros(date.getSeconds(), 2);
216 | case links.Timeline.StepDate.SCALE.WEEKDAY: return options.DAYS_SHORT[date.getDay()] + ' ' + date.getDate();
217 | case links.Timeline.StepDate.SCALE.DAY: return String(date.getDate());
218 | case links.Timeline.StepDate.SCALE.MONTH: return options.MONTHS_SHORT[date.getMonth()]; // month is zero based
219 | case links.Timeline.StepDate.SCALE.YEAR: return String(date.getFullYear());
220 | default: return "";
221 | }
222 | };
223 |
224 | timeline.setOptions({
225 | 'min': timeToDate(0),
226 | 'start': timeToDate(0),
227 | 'editable': true,
228 | 'zoomMin': 1000,
229 | 'zoomMax': 1000 * 60 * 4,
230 | 'showMajorLabels': false,
231 | 'showCurrentTime': false,
232 | 'showCustomTime': true
233 | });
234 |
235 | var onedit = function () {
236 | $("div#eventDialog").dialog("option","title","Edit event").dialog("open");
237 | };
238 |
239 | // callback function for the add item
240 | var onadd = function () {
241 | $("div#eventDialog").dialog("option","title","Add event").dialog("open");
242 | };
243 |
244 | links.events.addListener(timeline, 'add', onadd);
245 | links.events.addListener(timeline, 'edit', onedit);
246 |
247 | timeline.draw([]);
248 | timeline.move(0);
249 |
250 | timeline.setCustomTime(timeToDate(0))
251 | });
252 |
253 | var pastEvents = [];
254 | var currentEvents = [];
255 |
256 | function resetSequencer() {
257 | currentEvents = [];
258 | pastEvents = [];
259 | }
260 |
261 | function evalSequencer() {
262 | var sorted = timeline.getData().slice(0).sort(function(a,b) {
263 | return (a.start < b.start ? -1 : 1);
264 | });
265 |
266 | var ctime = timeline.getCustomTime();
267 |
268 | for (var i = 0; i < sorted.length; i++) {
269 | var event = sorted[i];
270 |
271 | var start = event.start;
272 | var end = event.end;
273 |
274 | if(typeof(end) === "undefined") {
275 | end = start;
276 | }
277 |
278 | if(pastEvents.includes(i)) {
279 | continue;
280 | }
281 |
282 | //If event has started
283 | if(ctime > start) {
284 | if(!currentEvents.includes(i)) {
285 | currentEvents.push(i);
286 |
287 | console.log("Start: " + event.content);
288 | events[event.event].start(event);
289 | }
290 | }
291 |
292 | if(ctime > end) {
293 | if(currentEvents.includes(i)) {
294 | pastEvents.push(i);
295 |
296 | console.log("End: " + event.content);
297 | events[event.event].end(event);
298 | }
299 | }
300 | }
301 | }
302 |
303 | function getSelectedRow() {
304 | var row = undefined;
305 | var sel = timeline.getSelection();
306 |
307 | if (sel.length) {
308 | if (sel[0].row != undefined) {
309 | row = sel[0].row;
310 | }
311 | }
312 |
313 | return row;
314 | }
315 |
316 | function updateSelectedRow(newRow) {
317 | var row = getSelectedRow();
318 |
319 | $.extend(timeline.getData()[row], newRow);
320 |
321 | timeline.redraw();
322 | }
323 |
324 | function clearTimeline() {
325 | timeline.deleteAllItems();
326 | }
327 |
328 | function saveTimeline() {
329 | if(typeof(songID) === "undefined" || songID === null || songID === "") {
330 | return;
331 | }
332 |
333 | storage.setValue(songID, timeline.getData());
334 | }
335 |
336 | function loadTimeline() {
337 | if(typeof(songID) === "undefined" || songID === null || songID === "") {
338 | return;
339 | }
340 |
341 | //Clear sequencer
342 | resetSequencer();
343 |
344 | storage.getValue(songID)
345 | .done(function(data) {
346 | var parsed = JSON.parse(data);
347 |
348 | if(parsed === null) {
349 | timeline.setData([]);
350 | return;
351 | }
352 |
353 | for (var i = parsed.length - 1; i >= 0; i--) {
354 | if(typeof(parsed[i].start) !== "undefined") {
355 | parsed[i].start = new Date(parsed[i].start);
356 | }
357 |
358 | if(typeof(parsed[i].end) !== "undefined") {
359 | parsed[i].end = new Date(parsed[i].end);
360 | }
361 | }
362 |
363 | timeline.setData(parsed);
364 | });
365 | }
366 |
367 | obs_scenes = [];
368 | obs_sources = [];
369 | function refreshOBS() {
370 | obs.GetSceneList().then(function(data) {obs_scenes = data.scenes});
371 | obs.GetSourcesList().then(function(data) {obs_sources = data.sources});
372 | }
373 |
374 | function timeToDate(tSeconds) {
375 | var hh = Math.floor(tSeconds / 3600);
376 | var mm = Math.floor((tSeconds - (hh * 3600)) / 60);
377 | var ss = Math.floor(tSeconds % 60);
378 | var ms = (tSeconds - Math.floor(tSeconds)) * 1000;
379 |
380 | return new Date(2000,0,1,hh,mm,ss,ms)
381 | }
382 |
383 | function dateToTime(date) {
384 | return (date.getMilliseconds() / 1000) + date.getSeconds() + (date.getMinutes() * 60) + (date.getHours() * 60 * 60);
385 | }
386 |
387 | //Convert a number to a duration "hh:mm:ss"
388 | function durationString(tSeconds) {
389 | var hh = Math.floor(tSeconds / 3600);
390 | var mm = Math.floor((tSeconds - (hh * 3600)) / 60);
391 | var ss = Math.floor(tSeconds % 60);
392 |
393 | if(hh < 10) {hh = "0"+hh;}
394 | if(mm < 10) {mm = "0"+mm;}
395 | if(ss < 10) {ss = "0"+ss;}
396 |
397 | if(hh > 0) {
398 | return hh+":"+mm+":"+ss;
399 | } else {
400 | return mm+":"+ss;
401 | }
402 | }
403 |
404 | function ialphabetically(a,b) {
405 | a = a.toUpperCase();
406 | b = b.toUpperCase();
407 |
408 | return a.localeCompare(b);
409 | }
--------------------------------------------------------------------------------
/addons/_deps/playthrough-tracker.js:
--------------------------------------------------------------------------------
1 | class PlaythroughTracker {
2 | //Create container for tracker data
3 | constructor(poller) {
4 | this.storage = new SnifferStorage("playthrough_tracker");
5 | this.poller = poller;
6 |
7 | this.previousBest = null;
8 | this.currentAttempt = null;
9 | this.currentAttemptPhrase = null;
10 |
11 | poller.onData((data) => this.onData(data));
12 | poller.onSongStarted((song) => this.onSongStarted(song));
13 | poller.onSongEnded((song) => this.onSongEnded(song));
14 | }
15 |
16 | //Check if song has previous best
17 | hasPreviousBest() {
18 | return this.previousBest != null;
19 | }
20 |
21 | //Check if better than previous best
22 | isBetter() {
23 | if(!this.hasPreviousBest()) {
24 | return true;
25 | }
26 |
27 | return this.getFinal().RelativeAccuracy >= 0;
28 | }
29 |
30 | //Check if better than previous best
31 | isBetterRelative(time) {
32 | if(!this.hasPreviousBest()) {
33 | return true;
34 | }
35 |
36 | return this.currentAttempt.compareTo(this.previousBest, time, this.poller);
37 | }
38 |
39 | //Get section accuracy
40 | getSectionAccuracy(time){
41 | return this.currentAttempt.getAcc(time, this.poller);
42 | }
43 |
44 | //Get phrase accuracy
45 | getPhraseAccuracy(time){
46 | return this.currentAttemptPhrase.getAcc(time, this.poller);
47 | }
48 |
49 | //Get phrase grade
50 | getPhraseGrade(time) {
51 | return this.currentAttemptPhrase.getGrade(time, this.poller);
52 | }
53 |
54 | //Get stats at end
55 | getFinal() {
56 | return this.currentAttempt.getFinal(this.previousBest)
57 | }
58 |
59 | //Get relative performance if has previous best
60 | getRelative(time) {
61 | if(!this.hasPreviousBest()) {
62 | return null;
63 | }
64 |
65 | return this.currentAttempt.getRelative(this.previousBest, time, this.poller);
66 | }
67 |
68 | //Do onData
69 | onData(data) {
70 | var state = data.currentState;
71 |
72 | if(state == STATE_SONG_STARTING || state == STATE_SONG_PLAYING || state == STATE_SONG_ENDING) {
73 | if(this.currentAttempt != null) {
74 | this.currentAttempt.update(data, this.poller);
75 | }
76 |
77 | if(this.currentAttemptPhrase != null){
78 | this.currentAttemptPhrase.update_phrase(data, this.poller);
79 | }
80 | }
81 | }
82 |
83 | //Do OnSongStarted
84 | onSongStarted(song) {
85 | this.previousBest = null;
86 |
87 | var arrangement = this.poller.getCurrentArrangement();
88 | this.currentAttempt = new PlaythroughBySection(arrangement.sections);
89 | this.currentAttemptPhrase = new PlaythroughByPhrase(arrangement.phraseIterations);
90 |
91 | var arr_id = arrangement.arrangementID;
92 |
93 | this.storage.getValue(song.songID+"_"+arr_id).done((data) => {
94 | var parsed = JSON.parse(data);
95 |
96 | if(parsed != null) {
97 | this.previousBest = parsed;
98 | console.log("Loaded previous best");
99 | console.log(this.previousBest);
100 | }
101 | });
102 | }
103 |
104 | //Do on song ended
105 | onSongEnded(song) {
106 | var arrangement = this.poller.getCurrentArrangement();
107 | var arr_id = arrangement.arrangementID;
108 |
109 | var finalReadout = this.poller.getCurrentReadout();
110 |
111 | this.currentAttempt.finalize(finalReadout);
112 |
113 | if(this.previousBest == null) {
114 | console.log("Storing first attempt");
115 | console.log(this.currentAttempt);
116 | this.storage.setValue(song.songID+"_"+arr_id, this.currentAttempt);
117 | } else if(this.isBetter()) {
118 | console.log("Storing better attempt");
119 | console.log(this.currentAttempt);
120 | this.storage.setValue(song.songID+"_"+arr_id, this.currentAttempt);
121 | } else {
122 | console.log("Not storing worse attempt");
123 | }
124 | }
125 | }
126 |
127 | class PlaythroughBySection {
128 |
129 | //Create storage for section variables
130 | constructor(sections) {
131 | this.sections = [];
132 |
133 | for (var i = sections.length - 1; i >= 0; i--) {
134 | this.sections[i] = {}
135 | }
136 |
137 | this.currentSection = 0;
138 | }
139 |
140 | //Get final stats
141 | getFinal(other) {
142 | var finalAccuracy = this.sections[this.sections.length-1].Accuracy;
143 |
144 | if(other == null) {
145 | return {
146 | CurrentAccuracy: finalAccuracy,
147 | PreviousAccuracy: 0
148 | }
149 | }
150 |
151 | var otherAccuracy = other.sections[other.sections.length-1].Accuracy;
152 |
153 | return {
154 | CurrentAccuracy: finalAccuracy,
155 | PreviousAccuracy: otherAccuracy,
156 | RelativeAccuracy: finalAccuracy - otherAccuracy
157 | }
158 | }
159 |
160 | //Compare to previous best
161 | compareTo(other, time, poller) {
162 | var sectionRelative = this.getRelative(other, time, poller);
163 |
164 | return sectionRelative.Accuracy >= 0;
165 | }
166 |
167 | //Get relative improvement/worsening
168 | getRelative(other, time, poller) {
169 | var index = poller.getSectionAt(time).index;
170 |
171 | var csection = this._calculateSectionStats(index, this.sections);
172 | var osection = this._calculateSectionStats(index, other.sections);
173 |
174 | return {
175 | Accuracy: csection.Accuracy - osection.Accuracy,
176 | TotalNotesHit: csection.TotalNotesHit - osection.TotalNotesHit,
177 | TotalNotesMissed: csection.TotalNotesMissed - osection.TotalNotesMissed,
178 | TotalNotes: csection.TotalNotes - osection.TotalNotes
179 | }
180 | }
181 |
182 | //get section accuracy
183 | getAcc(time, poller) {
184 | var index = poller.getSectionAt(time).index;
185 | var sectionAcc = this._calculateSectionStats(index, this.sections);
186 | return sectionAcc.Accuracy;
187 | }
188 |
189 | //calculate section stats
190 | _calculateSectionStats(index, sections) {
191 | var section = sections[index];
192 |
193 | var prevHitNotes = 0;
194 | var prevMissedNotes = 0;
195 | var prevTotalNotes = 0;
196 |
197 | if(index > 0) {
198 | prevHitNotes = sections[index-1].TotalNotesHit;
199 | prevMissedNotes = sections[index-1].TotalNotesMissed;
200 | prevTotalNotes = sections[index-1].TotalNotes;
201 |
202 | }
203 |
204 | var sectionHitNotes = section.TotalNotesHit - prevHitNotes;
205 | var sectionMissedNotes = section.TotalNotesMissed - prevMissedNotes;
206 | var sectionTotalNotes = section.TotalNotes - prevTotalNotes;
207 |
208 | var sectionAccuracy = 'Rest';
209 | if(sectionTotalNotes > 0) {
210 | if(sectionHitNotes > 0) {
211 | sectionAccuracy = sectionHitNotes / sectionTotalNotes * 100;
212 | }
213 | else {
214 | sectionAccuracy = 0;
215 | }
216 | }
217 |
218 | return {
219 | Accuracy: sectionAccuracy,
220 | TotalNotesHit: sectionHitNotes,
221 | TotalNotesMissed: sectionMissedNotes,
222 | TotalNotes: sectionTotalNotes
223 | }
224 | }
225 |
226 | //Update the info
227 | update(data, poller) {
228 | var cs = poller.getCurrentSection();
229 | if(cs == null) {return;}
230 |
231 | var csid = cs.index;
232 |
233 | if(csid > this.currentSection) {
234 | this.onSectionFinished(this.currentSection, data.memoryReadout.noteData);
235 | console.log("finished section",this.currentSection, "started section", csid);
236 | this.currentSection = csid;
237 | }
238 | }
239 |
240 | //finalize info for storage
241 | finalize(readout) {
242 | this.onSectionFinished(this.currentSection, readout.noteData);
243 |
244 | delete this.currentSection;
245 | }
246 |
247 | //Pull updated info on section end
248 | onSectionFinished(sectionId, noteData) {
249 | this.sections[sectionId] = {
250 | Accuracy: noteData.Accuracy,
251 | TotalNotesHit: noteData.TotalNotesHit,
252 | TotalNotesMissed: noteData.TotalNotesMissed,
253 | TotalNotes: noteData.TotalNotes,
254 | }
255 | }
256 |
257 | }
258 |
259 |
260 | class PlaythroughByPhrase {
261 |
262 | //Create structure for storing phrase info
263 | constructor(phraseIterations) {
264 | this.phraseIterations = [];
265 |
266 | for (var i = phraseIterations.length - 1; i >= 0; i--) {
267 | this.phraseIterations[i] = {}
268 | }
269 |
270 | this.currentPhrase = 0;
271 | }
272 |
273 | //Get phrase grade
274 | getGrade(time, poller) {
275 | var index = poller.getPhraseAt(time).index;
276 | var phraseGrade = this._calculatePhraseStats(index, this.phraseIterations);
277 | return phraseGrade.Grade;
278 | }
279 |
280 | //Get phrase accuracy
281 | getAcc(time, poller) {
282 | var index = poller.getPhraseAt(time).index;
283 | var phraseAcc = this._calculatePhraseStats(index, this.phraseIterations);
284 | return phraseAcc.Accuracy;
285 | }
286 |
287 | //calculate other phrase stats
288 | _calculatePhraseStats(index, phraseIterations) {
289 | var phrase = phraseIterations[index];
290 |
291 | var prevHitNotes = 0;
292 | var prevMissedNotes = 0;
293 | var prevTotalNotes = 0;
294 | var prevPerfPhra = 0;
295 | var prevGoodPhra = 0;
296 | var prevPassPhra = 0;
297 | var prevFailPhra = 0;
298 |
299 | if(index > 0) {
300 | prevHitNotes = phraseIterations[index-1].TotalNotesHit;
301 | prevMissedNotes = phraseIterations[index-1].TotalNotesMissed;
302 | prevTotalNotes = phraseIterations[index-1].TotalNotes;
303 | prevPerfPhra = phraseIterations[index-1].PerfectPhrases;
304 | prevGoodPhra = phraseIterations[index-1].GoodPhrases;
305 | prevPassPhra = phraseIterations[index-1].PassedPhrases;
306 | prevFailPhra = phraseIterations[index-1].FailedPhrases;
307 | }
308 |
309 | var phraseHitNotes = phrase.TotalNotesHit - prevHitNotes;
310 | var phraseMissedNotes = phrase.TotalNotesMissed - prevMissedNotes;
311 | var phraseTotalNotes = phrase.TotalNotes - prevTotalNotes;
312 | var phrasePerf = phrase.PerfectPhrases - prevPerfPhra;
313 | var phraseGood = phrase.GoodPhrases - prevGoodPhra;
314 | var phrasePass = phrase.PassedPhrases - prevPassPhra;
315 | var phraseFail = phrase.FailedPhrases - prevFailPhra;
316 |
317 | var phraseAccuracy = 'Rest';
318 | if(phraseTotalNotes > 0) {
319 | if(phraseHitNotes > 0) {
320 | phraseAccuracy = phraseHitNotes / phraseTotalNotes * 100;
321 | }
322 | else {
323 | phraseAccuracy = 0;
324 | }
325 | }
326 |
327 | var phraseGrade = 'No Data'
328 | if(phrasePerf > 0){
329 | var phraseGrade = 'Perfect';
330 | } else if (phraseGood > 0){
331 | var phraseGrade = 'Good';
332 | } else if (phrasePass > 0){
333 | var phraseGrade = 'Pass';
334 | } else if (phraseFail > 0){
335 | var phraseGrade = 'Fail'
336 | } else if(phraseTotalNotes == 0){
337 | var phraseGrade = 'Rest';
338 | }
339 |
340 | return {
341 | Accuracy: phraseAccuracy,
342 | TotalNotesHit: phraseHitNotes,
343 | TotalNotesMissed: phraseMissedNotes,
344 | TotalNotes: phraseTotalNotes,
345 | PerfectPhrase: phrasePerf,
346 | GoodPhrase: phraseGood,
347 | PassedPhrase: phrasePass,
348 | FailedPhrase: phraseFail,
349 | Grade: phraseGrade
350 | }
351 | }
352 |
353 | //update phrase
354 | update_phrase(data, poller) {
355 | var cp = poller.getCurrentPhrase();
356 | if(cp == null) {return;}
357 |
358 | var cpid = cp.index;
359 |
360 | if(cpid > this.currentPhrase) {
361 | this.onPhraseFinished(this.currentPhrase, data.memoryReadout.noteData);
362 | console.log("finished phrase",this.currentPhrase, "started phrase", cpid);
363 | this.currentPhrase = cpid;
364 | }
365 | }
366 |
367 | //Finalize for readout
368 | finalize_phrase(readout) {
369 | this.onPhraseFinished(this.currentPhrase, readout.noteData);
370 |
371 | delete this.currentPhrase;
372 | }
373 |
374 | //get info at end of phrase
375 | onPhraseFinished(phraseId, noteData) {
376 | this.phraseIterations[phraseId] = {
377 | Accuracy: noteData.Accuracy,
378 | TotalNotesHit: noteData.TotalNotesHit,
379 | TotalNotesMissed: noteData.TotalNotesMissed,
380 | TotalNotes: noteData.TotalNotes,
381 | PerfectPhrases: noteData.PerfectPhrases,
382 | GoodPhrases: noteData.GoodPhrases,
383 | PassedPhrases: noteData.PassedPhrases,
384 | FailedPhrases: noteData.FailedPhrases
385 | }
386 | }
387 | }
388 |
--------------------------------------------------------------------------------