├── .gitattributes ├── .gitignore ├── OpenSongChart.sln ├── README.md └── SongFormat ├── SerializationUtil.cs ├── SongFormat.cs ├── SongFormat.projitems └── SongFormat.shproj /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /OpenSongChart.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34024.191 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SongFormat", "SongFormat\SongFormat.shproj", "{595D9376-81D5-4163-9045-BD1D72A45B16}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionProperties) = preSolution 10 | HideSolutionNode = FALSE 11 | EndGlobalSection 12 | GlobalSection(ExtensibilityGlobals) = postSolution 13 | SolutionGuid = {5634E2AE-378D-4FD6-8C44-9E15716AFC6E} 14 | EndGlobalSection 15 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 16 | SongFormat\SongFormat.projitems*{595d9376-81d5-4163-9045-bd1d72a45b16}*SharedItemsImports = 13 17 | EndGlobalSection 18 | EndGlobal 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is it? 2 | This is a work-in-progress open format for song charts - the data required to capture instrument performances that make up a song. 3 | 4 | Design goals are: 5 | - Make it easy to export to (ie: from a program for editing song charts). 6 | - Make it easy to parse. 7 | - Make it easy to render a visual timeline representation ("note highway") from. 8 | 9 | # What is it not? 10 | - ***It is a ".pdf", not a ".doc".*** 11 | - It is not a score format. It is designed to represent a song as actually performed, with events synchronized to an audio recording. 12 | - It is not an "editor" format. It is designed for playback. 13 | 14 | # How to get songs in OpenSongChart format 15 | 16 | Songs in various formats (Rocksmith PSARC, Rock Band) can be converted using the [ChartConverter](https://github.com/mikeoliphant/ChartConverter) application. 17 | 18 | # What is in this repo? 19 | This repository currently has C# data structures designed to be deserialized from data files (currently using json). 20 | 21 | # Song format details 22 | A song is comprised of a folder of files. 23 | 24 | Files are: 25 | - **song.json** (song metadata) 26 | - **song.ogg** (song audio recording) 27 | - **arrangement.json** (song structure information - currently just measures/beats) 28 | - **\.json** (one file per instrument part) 29 | 30 | ## song.json 31 | 32 | ``` 33 | { 34 | "SongName": "Cool Song", 35 | "ArtistName": "Cool Artist", 36 | "AlbumName": "Awesome album", 37 | "A440CentsOffset": 30, 38 | "InstrumentParts": [ 39 | { 40 | "InstrumentName": "bass", 41 | "InstrumentType": "BassGuitar", 42 | "Tuning": { 43 | "StringSemitoneOffsets": [ 44 | -1, 45 | -1, 46 | -1, 47 | -1, 48 | -1 49 | ] 50 | } 51 | }, 52 | { 53 | "InstrumentName": "lead", 54 | "InstrumentType": "LeadGuitar", 55 | "Tuning": { 56 | "StringSemitoneOffsets": [ 57 | -1, 58 | -1, 59 | -1, 60 | -1, 61 | -1 62 | ] 63 | } 64 | }, 65 | { 66 | "InstrumentName": "rhythm", 67 | "InstrumentType": "RhythmGuitar", 68 | "Tuning": { 69 | "StringSemitoneOffsets": [ 70 | -1, 71 | -1, 72 | -1, 73 | -1, 74 | -1 75 | ] 76 | } 77 | }, 78 | { 79 | "InstrumentName": "vocals", 80 | "InstrumentType": "Vocals" 81 | } 82 | ] 83 | } 84 | ``` 85 | 86 | Notes: 87 | 88 | "**A440CentsOffset**" is the numbers of cents (100th of a semitone) the song is out of tune. This is in addition to the individual instrument tunings described by the "**Tuning**" for each part. "**StringSemitoneOffsets**" specifies how many semitones each string is detuned from E standard tuning. So, for this song example, all of the guitar parts are tuned a half-step down to Eb. But the song is also 30 cents sharp. 89 | 90 | "**InstrumentType**" is an enumeration. It is separate from the name of the part, because there could be more than one part for an instrument type. Current types are: 91 | 92 | ``` 93 | LeadGuitar 94 | RhythmGuitar 95 | BassGuitar 96 | Vocals 97 | ``` 98 | 99 | ## arrangement.json 100 | The arrangment file currently just has a "**Beats**" section, which is a list of time-indexed measures/beats. 101 | 102 | ## \.json 103 | The individual stringed instrument parts have a "**Sections**" list, a "**Chords**" list and a "**Notes**" list. 104 | 105 | Like this: 106 | 107 | ``` 108 | { 109 | "Sections": [ 110 |
, 111 |
112 | ... 113 | ], 114 | "Chords": [ 115 | 116 | 117 | ... 118 | ], 119 | "Notes": [ 120 | 121 | 122 | ... 123 | ] 124 | } 125 | ``` 126 | 127 | A section is a division of an instrument part into logical sections. It looks like this: 128 | 129 | ``` 130 | { 131 | "Name": "intro", 132 | "StartTime": 4.18, 133 | "EndTime":19.737 134 | } 135 | ``` 136 | 137 | Chords indicate the fingers and frets per string (with "-1" indicating the string is unused) 138 | 139 | ``` 140 | { 141 | "Name": "A", 142 | "Fingers": [-1,-1,1,2,3,-1], 143 | "Frets":[-1,0,2,2,2,-1] 144 | } 145 | ``` 146 | In this case, it is an "A" chord with fingers 1, 2, and 3 all on fret 2 of the D, G, and B strings and the A string is open. 147 | 148 | A note is an individual note event. It is the most complex structure. 149 | 150 | Here is an example: 151 | 152 | ``` 153 | { 154 | "TimeOffset": 43.796, 155 | "TimeLength": 0.204, 156 | "Fret": 5, 157 | "String": 1, 158 | "Techniques": "Slide", 159 | "HandFret": 5, 160 | "SlideFret": 7 161 | } 162 | ``` 163 | "TimeOffset" is the start time of the note in seconds. "TimeLength" is the duration. "Fret" and "String" are the fingering. "HandFret" is the hand anchor position. "Techniques" is a comma-separated list of note modifiers. In this case, we have a slide from from 5 to fret 7 on the second string. 164 | 165 | Currently, these are the possible techniques: 166 | 167 | ``` 168 | HammerOn 169 | PullOff 170 | Accent 171 | PalmMute 172 | FretHandMute 173 | Slide 174 | Bend 175 | Tremolo 176 | Vibrato 177 | Harmonic 178 | PinchHarmonic 179 | Tap 180 | Slap 181 | Pop 182 | Chord 183 | ChordNote 184 | Continued 185 | ``` 186 | 187 | These should be pretty self-explanatory, with a few exceptions: 188 | - **Chord** indicates the note is a chord, **ChordID** will be an index into the list of Chords 189 | - **ChordNote** if present with **Chord**, indicates that individual notes for the chord follow with their own separate information 190 | - **ChordNote** if *not* present with **Chord**, indicates the chord is a simple chord 191 | - **Continued** indicates the note is a continuation of a previous note (so a new note head should not be drawn) 192 | 193 | ## vocals.json 194 | Vocal parts have a different structure. They are simply a list of timed vocal events: 195 | 196 | ``` 197 | { 198 | "Vocal": "Lala\n" 199 | "TimeOffset":16.159 200 | } 201 | ``` 202 | 203 | Newline characters ("\n") indicate a line break. Dashes ("-") indicate word breaks. Any vocal that doesn't end with "-" should be considered a complete word. 204 | -------------------------------------------------------------------------------- /SongFormat/SerializationUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Text.Json.Serialization.Metadata; 5 | using System.Text.Json.Serialization; 6 | using System.Text.Json; 7 | using System.Text.RegularExpressions; 8 | using System.Globalization; 9 | 10 | namespace SongFormat 11 | { 12 | public static class SerializationUtil 13 | { 14 | public static string GetSafeFilename(string path) 15 | { 16 | return Regex.Replace(path, "[^a-zA-Z0-9]", String.Empty).Trim(); 17 | } 18 | 19 | public static JsonSerializerOptions IndentedSerializerOptions { get; private set; } = new JsonSerializerOptions() 20 | { 21 | Converters = { 22 | new JsonStringEnumConverter(), 23 | new JsonConverterFloatRound() 24 | }, 25 | TypeInfoResolver = new DefaultJsonTypeInfoResolver 26 | { 27 | Modifiers = { DefaultValueModifier } 28 | }, 29 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, 30 | WriteIndented = true, 31 | }; 32 | 33 | public static JsonSerializerOptions CondensedSerializerOptions { get; private set; } = new JsonSerializerOptions() 34 | { 35 | Converters = { 36 | new JsonStringEnumConverter(), 37 | new JsonConverterFloatRound() 38 | }, 39 | TypeInfoResolver = new DefaultJsonTypeInfoResolver 40 | { 41 | Modifiers = { DefaultValueModifier } 42 | }, 43 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault 44 | }; 45 | 46 | private class JsonConverterFloatRound : JsonConverter 47 | { 48 | public override float Read(ref Utf8JsonReader reader, 49 | Type typeToConvert, JsonSerializerOptions options) 50 | { 51 | return reader.GetSingle(); 52 | } 53 | 54 | public override void Write(Utf8JsonWriter writer, float value, 55 | JsonSerializerOptions options) 56 | { 57 | writer.WriteRawValue(value.ToString("0.###", 58 | CultureInfo.InvariantCulture)); 59 | } 60 | } 61 | 62 | private static void DefaultValueModifier(JsonTypeInfo typeInfo) 63 | { 64 | if (typeInfo.Kind != JsonTypeInfoKind.Object) 65 | return; 66 | 67 | foreach (var property in typeInfo.Properties) 68 | if (property.PropertyType == typeof(int)) 69 | { 70 | property.ShouldSerialize = (_, val) => ((int)val != -1); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SongFormat/SongFormat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace SongFormat 7 | { 8 | /// 9 | /// Top level song metadata 10 | /// 11 | public class SongData 12 | { 13 | public string SongName { get; set; } 14 | public int SongYear { get; set; } 15 | public float SongLengthSeconds { get; set; } 16 | public string ArtistName { get; set; } 17 | public string AlbumName { get; set; } 18 | public float A440CentsOffset { get; set; } 19 | public List InstrumentParts { get; set; } = new List(); 20 | 21 | public SongInstrumentPart GetPart(string instrumentName) 22 | { 23 | return InstrumentParts.FirstOrDefault(p => p.InstrumentName == instrumentName); 24 | } 25 | 26 | public void AddOrReplacePart(SongInstrumentPart part) 27 | { 28 | InstrumentParts.RemoveAll(p => (p.InstrumentName == part.InstrumentName)); 29 | 30 | InstrumentParts.Add(part); 31 | } 32 | 33 | public override string ToString() 34 | { 35 | return ArtistName + " - " + SongName; 36 | } 37 | } 38 | 39 | /// 40 | /// Instrument part metadata 41 | /// 42 | public class SongInstrumentPart 43 | { 44 | public string InstrumentName { get; set; } 45 | [JsonIgnore(Condition = JsonIgnoreCondition.Never)] 46 | public ESongInstrumentType InstrumentType { get; set; } 47 | public string ArrangementName { get; set; } 48 | public string SongAudio { get; set; } 49 | public StringTuning Tuning { get; set; } 50 | public int CapoFret { get; set; } = 0; 51 | 52 | public override string ToString() 53 | { 54 | return InstrumentName + ((Tuning == null) ? "" : " (" + Tuning.GetTuning() + ((CapoFret > 0) ? (" C" + CapoFret) : "") + ")"); 55 | } 56 | } 57 | 58 | /// 59 | /// Song structure/arrangement information 60 | /// 61 | public class SongStructure 62 | { 63 | public List Sections { get; set; } = new List(); 64 | public List Beats { get; set; } = new List(); 65 | } 66 | 67 | /// 68 | /// Song section (ie: "verse", "chorus") 69 | /// 70 | public class SongSection 71 | { 72 | public string Name { get; set; } 73 | public float StartTime { get; set; } 74 | public float EndTime { get; set; } 75 | 76 | public override string ToString() 77 | { 78 | return Name + "[" + StartTime + "-" + EndTime + "]"; 79 | } 80 | } 81 | 82 | public interface ISongEvent 83 | { 84 | public float TimeOffset { get; } 85 | public float EndTime { get; } 86 | } 87 | 88 | /// 89 | /// An individual beat in a song 90 | /// 91 | public struct SongBeat : ISongEvent 92 | { 93 | [JsonIgnore(Condition = JsonIgnoreCondition.Never)] 94 | public float TimeOffset { get; set; } 95 | public bool IsMeasure { get; set; } 96 | public float EndTime => TimeOffset; 97 | } 98 | 99 | /// 100 | /// The type of instrument 101 | /// 102 | public enum ESongInstrumentType 103 | { 104 | LeadGuitar, 105 | RhythmGuitar, 106 | BassGuitar, 107 | Keys, 108 | Drums, 109 | Vocals 110 | } 111 | 112 | /// 113 | /// Tuning information for a stringed instrument 114 | /// 115 | public class StringTuning 116 | { 117 | public List StringSemitoneOffsets { get; set; } = null; 118 | 119 | public string GetTuning() 120 | { 121 | // If we don't have any offsets, assume E Standard 122 | if ((StringSemitoneOffsets == null) || (StringSemitoneOffsets.Count < 4)) 123 | return "E Std"; 124 | 125 | if (IsOffsetFromStandard()) 126 | { 127 | string key = (StringSemitoneOffsets[1] < 0) ? GetOffsetNoteFlat(StringSemitoneOffsets[1]) : GetOffsetNoteSharp(StringSemitoneOffsets[1]); 128 | 129 | if (key == null) 130 | return GetTuningAsNotes(); 131 | 132 | if (StringSemitoneOffsets[0] == StringSemitoneOffsets[1]) 133 | { 134 | return key + " Std"; 135 | } 136 | else // Drop tuning 137 | { 138 | string drop = GetOffsetNoteFlat(StringSemitoneOffsets[0]); 139 | 140 | if (drop == null) 141 | return GetTuningAsNotes(); 142 | 143 | if (key == "E") 144 | return "Drop " + drop; 145 | 146 | return key + " Drop " + drop; 147 | } 148 | } 149 | 150 | return GetTuningAsNotes(); 151 | } 152 | 153 | static int[] StringOffsetsFromE = { 0, 5, 10, 3, 7, 0 }; 154 | 155 | public string GetTuningAsNotes() 156 | { 157 | string tuning = null; 158 | 159 | for (int i = 0; i < StringSemitoneOffsets.Count; i++) 160 | { 161 | tuning += GetOffsetNoteSharp(StringSemitoneOffsets[i] + StringOffsetsFromE[i]); 162 | } 163 | 164 | switch (tuning) 165 | { 166 | case "DGDGBD": 167 | return "Open G"; 168 | 169 | case "DADF#AD": 170 | return "Open D"; 171 | 172 | case "EBEG#BE": 173 | return "Open E"; 174 | 175 | case "EAEAC#E": 176 | return "Open A"; 177 | 178 | case "CGCGCE": 179 | return "Open C"; 180 | } 181 | 182 | return tuning; 183 | } 184 | 185 | /// 186 | /// Check if a tuning is offset from standard tuning (including first-string drop tunings) 187 | /// 188 | /// Whether the tuning is offset from standard 189 | public bool IsOffsetFromStandard() 190 | { 191 | for (int i = 2; i < StringSemitoneOffsets.Count; i++) 192 | if (StringSemitoneOffsets[1] != StringSemitoneOffsets[i]) 193 | return false; 194 | 195 | return true; 196 | } 197 | 198 | /// 199 | /// Get note name offset from E using sharps 200 | /// 201 | /// The offset in semitones 202 | /// The offset note name 203 | public static string GetOffsetNoteSharp(int offset) 204 | { 205 | if (offset < 0) 206 | offset += 12; 207 | 208 | switch (offset % 12) 209 | { 210 | case 0: 211 | return "E"; 212 | case 1: 213 | return "F"; 214 | case 2: 215 | return "F#"; 216 | case 3: 217 | return "G"; 218 | case 4: 219 | return "G#"; 220 | case 5: 221 | return "A"; 222 | case 6: 223 | return "A#"; 224 | case 7: 225 | return "B"; 226 | case 8: return "C"; 227 | case 9: 228 | return "C#"; 229 | case 10: 230 | return "D"; 231 | case 11: 232 | return "D#"; 233 | } 234 | 235 | return null; 236 | } 237 | 238 | /// 239 | /// Get note name offset from E using flats 240 | /// 241 | /// The offset in semitones 242 | /// The offset note name 243 | public static string GetOffsetNoteFlat(int offset) 244 | { 245 | if (offset < 0) 246 | offset += 12; 247 | 248 | switch (offset % 12) 249 | { 250 | case 0: 251 | return "E"; 252 | case 1: 253 | return "F"; 254 | case 2: 255 | return "Gb"; 256 | case 3: 257 | return "G"; 258 | case 4: 259 | return "Ab"; 260 | case 5: 261 | return "A"; 262 | case 6: 263 | return "Bb"; 264 | case 7: 265 | return "B"; 266 | case 8: return "C"; 267 | case 9: 268 | return "Db"; 269 | case 10: 270 | return "D"; 271 | case 11: 272 | return "Eb"; 273 | } 274 | 275 | return null; 276 | } 277 | 278 | public override string ToString() 279 | { 280 | return GetTuning(); 281 | } 282 | } 283 | 284 | /// 285 | /// Notes and chords for an instrument part 286 | /// 287 | public class SongInstrumentNotes 288 | { 289 | public List Sections { get; set; } = new List(); 290 | public List Chords { get; set; } = new List(); 291 | public List Notes { get; set; } = new List(); 292 | } 293 | 294 | /// 295 | /// Chord notes/fingering 296 | /// 297 | public class SongChord 298 | { 299 | public string Name { get; set; } 300 | public List Fingers { get; set; } = new List(); 301 | public List Frets { get; set;} = new List(); 302 | } 303 | 304 | /// 305 | /// An individual note/chord event in a song 306 | /// 307 | public struct SongNote : ISongEvent 308 | { 309 | /// 310 | /// Start offset of the note in seconds 311 | /// 312 | [JsonIgnore(Condition = JsonIgnoreCondition.Never)] 313 | public float TimeOffset { get; set; } = 0; 314 | /// 315 | /// Sustain length of the note in seconds 316 | /// 317 | public float TimeLength { get; set; } = 0; 318 | /// 319 | /// Fret number of the note - "0" is open string, "-1" is unfretted 320 | /// 321 | public int Fret { get; set; } = -1; 322 | /// 323 | /// String of the note (zero-based) 324 | /// 325 | public int String { get; set; } = -1; 326 | /// 327 | /// Array of bend offsets 328 | /// 329 | public CentsOffset[] CentsOffsets { get; set; } = null; 330 | /// 331 | /// Song technique flags 332 | /// 333 | public ESongNoteTechnique Techniques { get; set; } = 0; 334 | /// 335 | /// Bottom fret of hand position 336 | /// 337 | public int HandFret { get; set; } = -1; 338 | /// 339 | /// Fret that note slides to over the course of its sustain 340 | /// 341 | public int SlideFret { get; set; } = -1; 342 | /// 343 | /// Index into chord array to use for notes 344 | /// 345 | public int ChordID { get; set; } = -1; 346 | /// 347 | /// Index into chord array to use for fingering 348 | /// 349 | public int FingerID { get; set; } = -1; 350 | 351 | public float EndTime => TimeOffset + TimeLength; 352 | 353 | public SongNote() 354 | { 355 | 356 | } 357 | } 358 | 359 | /// 360 | /// Offset structure for bends 361 | /// 362 | public struct CentsOffset 363 | { 364 | /// 365 | /// Time offset of the bend position 366 | /// 367 | [JsonIgnore(Condition = JsonIgnoreCondition.Never)] 368 | public float TimeOffset { get; set; } 369 | /// 370 | /// Amount of the bend, in cents (100th of a semitone) 371 | /// 372 | public int Cents { get; set; } 373 | } 374 | 375 | /// 376 | /// Technique flags 377 | /// 378 | [Flags] 379 | public enum ESongNoteTechnique 380 | { 381 | HammerOn = 1 << 1, 382 | PullOff = 1 << 2, 383 | Accent = 1 << 3, 384 | PalmMute = 1 << 4, 385 | FretHandMute = 1 << 5, 386 | Slide = 1 << 6, 387 | Bend = 1 << 7, 388 | Tremolo = 1 << 8, 389 | Vibrato = 1 << 9, 390 | Harmonic = 1 << 10, 391 | PinchHarmonic = 1 << 11, 392 | Tap = 1 << 12, 393 | Slap = 1 << 13, 394 | Pop = 1 << 14, 395 | Chord = 1 << 15, 396 | ChordNote = 1 << 16, 397 | Continued = 1 << 17, 398 | Arpeggio = 1 << 18 399 | } 400 | 401 | /// 402 | /// Notes for a keys part 403 | /// 404 | public class SongKeyboardNotes 405 | { 406 | public List Sections { get; set; } = new List(); 407 | public List Notes { get; set; } = new List(); 408 | } 409 | 410 | public struct SongKeyboardNote : ISongEvent 411 | { 412 | [JsonIgnore(Condition = JsonIgnoreCondition.Never)] 413 | public float TimeOffset { get; set; } = 0; 414 | public float TimeLength { get; set; } = 0; 415 | public float EndTime => TimeOffset + TimeLength; 416 | public int Note { get; set; } = 0; 417 | public int Velocity { get; set; } = 0; 418 | 419 | public SongKeyboardNote() 420 | { 421 | 422 | } 423 | } 424 | 425 | public enum EDrumKitPieceType 426 | { 427 | None, 428 | Kick, 429 | Snare, 430 | HiHat, 431 | Crash, 432 | Ride, 433 | Tom, 434 | Flexi 435 | } 436 | 437 | public enum EDrumKitPiece 438 | { 439 | None, 440 | Kick, 441 | Snare, 442 | HiHat, 443 | Crash, 444 | Crash2, 445 | Crash3, 446 | Ride, 447 | Ride2, 448 | Tom1, 449 | Tom2, 450 | Tom3, 451 | Tom4, 452 | Tom5, 453 | Flexi1, 454 | Flexi2, 455 | Flexi3, 456 | Flexi4 457 | } 458 | 459 | public enum EDrumArticulation 460 | { 461 | None, 462 | DrumHead, 463 | DrumHeadEdge, 464 | DrumRim, 465 | SideStick, 466 | HiHatClosed, 467 | HiHatOpen, 468 | HiHatChick, 469 | HiHatSplash, 470 | CymbalEdge, 471 | CymbalBow, 472 | CymbalBell, 473 | CymbalChoke, 474 | FlexiA, 475 | FlexiB, 476 | FlexiC 477 | } 478 | 479 | public struct SongDrumNote : ISongEvent 480 | { 481 | public static EDrumKitPieceType GetKitPieceType(EDrumKitPiece kitPiece) 482 | { 483 | switch (kitPiece) 484 | { 485 | case EDrumKitPiece.None: 486 | return EDrumKitPieceType.None; 487 | case EDrumKitPiece.Kick: 488 | return EDrumKitPieceType.Kick; 489 | case EDrumKitPiece.Snare: 490 | return EDrumKitPieceType.Snare; 491 | case EDrumKitPiece.HiHat: 492 | return EDrumKitPieceType.HiHat; 493 | case EDrumKitPiece.Crash: 494 | case EDrumKitPiece.Crash2: 495 | case EDrumKitPiece.Crash3: 496 | return EDrumKitPieceType.Crash; 497 | case EDrumKitPiece.Ride: 498 | case EDrumKitPiece.Ride2: 499 | return EDrumKitPieceType.Ride; 500 | case EDrumKitPiece.Tom1: 501 | case EDrumKitPiece.Tom2: 502 | case EDrumKitPiece.Tom3: 503 | case EDrumKitPiece.Tom4: 504 | case EDrumKitPiece.Tom5: 505 | return EDrumKitPieceType.Tom; 506 | case EDrumKitPiece.Flexi1: 507 | case EDrumKitPiece.Flexi2: 508 | case EDrumKitPiece.Flexi3: 509 | case EDrumKitPiece.Flexi4: 510 | return EDrumKitPieceType.Flexi; 511 | } 512 | 513 | return EDrumKitPieceType.None; 514 | } 515 | 516 | public static EDrumArticulation GetDefaultArticulation(EDrumKitPiece kitPiece) 517 | { 518 | return GetDefaultArticulation(GetKitPieceType(kitPiece)); 519 | } 520 | 521 | public static EDrumArticulation GetDefaultArticulation(EDrumKitPieceType kitPieceType) 522 | { 523 | EDrumArticulation articulation = EDrumArticulation.None; 524 | 525 | switch (kitPieceType) 526 | { 527 | case EDrumKitPieceType.Kick: 528 | articulation = EDrumArticulation.DrumHead; 529 | break; 530 | case EDrumKitPieceType.Snare: 531 | articulation = EDrumArticulation.DrumHead; 532 | break; 533 | case EDrumKitPieceType.HiHat: 534 | articulation = EDrumArticulation.HiHatClosed; 535 | break; 536 | case EDrumKitPieceType.Crash: 537 | articulation = EDrumArticulation.CymbalEdge; 538 | break; 539 | case EDrumKitPieceType.Ride: 540 | articulation = EDrumArticulation.CymbalBow; 541 | break; 542 | case EDrumKitPieceType.Flexi: 543 | articulation = EDrumArticulation.FlexiA; 544 | break; 545 | case EDrumKitPieceType.Tom: 546 | articulation = EDrumArticulation.DrumHead; 547 | break; 548 | default: 549 | break; 550 | } 551 | 552 | return articulation; 553 | } 554 | 555 | [JsonIgnore(Condition = JsonIgnoreCondition.Never)] 556 | public float TimeOffset { get; set; } = 0; 557 | public float EndTime => TimeOffset; 558 | public EDrumKitPiece KitPiece { get; set; } 559 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 560 | public EDrumArticulation Articulation { get; set; } 561 | 562 | public SongDrumNote() 563 | { 564 | 565 | } 566 | } 567 | 568 | public class SongDrumNotes 569 | { 570 | public List Sections { get; set; } = new List(); 571 | public List Notes { get; set; } = new List(); 572 | } 573 | 574 | 575 | /// 576 | /// Vocal/lyric events in a song 577 | /// 578 | public struct SongVocal : ISongEvent 579 | { 580 | public string Vocal { get; set; } 581 | public float TimeOffset { get; set; } 582 | public float EndTime => TimeOffset; 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /SongFormat/SongFormat.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | 595d9376-81d5-4163-9045-bd1d72a45b16 7 | 8 | 9 | SongFormat 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SongFormat/SongFormat.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 595d9376-81d5-4163-9045-bd1d72a45b16 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------