├── 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 |
26 |
27 |
28 | 33 |

34 |
35 |

{{curEvent.desc}}

36 |
37 | 42 |

43 |
44 |
45 |
46 |
47 |
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 | --------------------------------------------------------------------------------