├── .gitignore ├── DumpOPL ├── DumpOPL.vcxproj ├── DumpOPL.vcxproj.filters ├── dump.c └── test.opb ├── LICENSE ├── OPB2WAV ├── OPB2WAV.vcxproj ├── OPB2WAV.vcxproj.filters ├── doom.opb ├── opb2wav.c └── opl.h ├── OPBinaryLib.sln ├── OPBinaryLib.vcxproj ├── OPBinaryLib.vcxproj.filters ├── OPBinarySharp ├── LICENSE ├── OPB.cs ├── OPBCommand.cs ├── OPBException.cs ├── OPBFormat.cs ├── OPBSharpTest │ ├── App.config │ ├── OPBSharpTest.csproj │ ├── Program.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ └── doom.vgm ├── OPBinarySharp.csproj ├── OPBinarySharp.sln ├── Properties │ └── AssemblyInfo.cs ├── README.md └── VGM.cs ├── README.md ├── opb_file_specification.txt ├── opblib.c └── opblib.h /.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 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /DumpOPL/DumpOPL.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | Win32Proj 24 | {f2c67507-a331-419a-b5a2-e91b1eb6002e} 25 | DumpOPL 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v142 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v142 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v142 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v142 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | false 78 | 79 | 80 | true 81 | 82 | 83 | false 84 | 85 | 86 | 87 | Level3 88 | true 89 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 90 | true 91 | 92 | 93 | Console 94 | true 95 | 96 | 97 | 98 | 99 | Level3 100 | true 101 | true 102 | true 103 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 104 | true 105 | 106 | 107 | Console 108 | true 109 | true 110 | true 111 | 112 | 113 | 114 | 115 | Level3 116 | true 117 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 118 | true 119 | 120 | 121 | Console 122 | true 123 | 124 | 125 | 126 | 127 | Level3 128 | true 129 | true 130 | true 131 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 132 | true 133 | 134 | 135 | Console 136 | true 137 | true 138 | true 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | {272e3ec8-1734-41db-b225-b5adaf95fdb9} 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /DumpOPL/DumpOPL.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | -------------------------------------------------------------------------------- /DumpOPL/dump.c: -------------------------------------------------------------------------------- 1 | /* 2 | // MIT License 3 | // 4 | // Copyright (c) 2021 Eniko Fox/Emma Maassen 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | */ 24 | #ifdef _WIN32 25 | #define _CRT_SECURE_NO_DEPRECATE 26 | #endif 27 | #include 28 | #include 29 | #include 30 | #include "..\opblib.h" 31 | 32 | void GetFilename(char* path, char* result, size_t maxLen) { 33 | int lastSlash = -1; 34 | int i = 0; 35 | while (path[i] != '\0') { 36 | if (path[i] == '/' || path[i] == '\\') lastSlash = i; 37 | i++; 38 | } 39 | int pathLen = i; 40 | 41 | if (lastSlash >= 0) strncpy(result, path + lastSlash + 1, (size_t)(maxLen - 1)); 42 | else strncpy(result, path, (size_t)(maxLen - 1)); 43 | result[maxLen - 1] = '\0'; 44 | } 45 | 46 | int ReceiveOpbBuffer(OPB_Command* commandStream, size_t commandCount, void* context) { 47 | for (size_t i = 0; i < commandCount; i++) { 48 | printf("%1.3f: 0x%03X, 0x%02X\n", commandStream[i].Time, commandStream[i].Addr, commandStream[i].Data); 49 | } 50 | return 0; 51 | } 52 | 53 | int main(int argc, char* argv[]) { 54 | if (argc < 2) { 55 | char* path = argv[0]; 56 | char filename[128]; 57 | GetFilename(path, filename, 128); 58 | 59 | printf("Usage: %s \n", filename); 60 | printf("Format is time: register, data\n"); 61 | exit(EXIT_FAILURE); 62 | } 63 | 64 | int error; 65 | if ((error = OPB_FileToOpl(argv[1], ReceiveOpbBuffer, NULL)) != 0) { 66 | printf("Error trying to dump OPL: %s\n", OPB_GetErrorMessage(error)); 67 | exit(EXIT_FAILURE); 68 | } 69 | } -------------------------------------------------------------------------------- /DumpOPL/test.opb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Enichan/OPBinaryLib/4334ec39c16c4d06d2cda529bc73b000d727f553/DumpOPL/test.opb -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eniko Fox/Emma Maassen 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 | -------------------------------------------------------------------------------- /OPB2WAV/OPB2WAV.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {272e3ec8-1734-41db-b225-b5adaf95fdb9} 30 | 31 | 32 | 33 | 16.0 34 | Win32Proj 35 | {d73a35ad-a776-463e-9e72-ef37ad628208} 36 | OPB2WAV 37 | 10.0 38 | 39 | 40 | 41 | Application 42 | true 43 | v142 44 | Unicode 45 | 46 | 47 | Application 48 | false 49 | v142 50 | true 51 | Unicode 52 | 53 | 54 | Application 55 | true 56 | v142 57 | Unicode 58 | 59 | 60 | Application 61 | false 62 | v142 63 | true 64 | Unicode 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | true 86 | 87 | 88 | false 89 | 90 | 91 | true 92 | 93 | 94 | false 95 | 96 | 97 | 98 | Level3 99 | true 100 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 101 | true 102 | 103 | 104 | Console 105 | true 106 | 107 | 108 | 109 | 110 | Level3 111 | true 112 | true 113 | true 114 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 115 | true 116 | 117 | 118 | Console 119 | true 120 | true 121 | true 122 | 123 | 124 | 125 | 126 | Level3 127 | true 128 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 129 | true 130 | 131 | 132 | Console 133 | true 134 | 135 | 136 | 137 | 138 | Level3 139 | true 140 | true 141 | true 142 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 143 | true 144 | 145 | 146 | Console 147 | true 148 | true 149 | true 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /OPB2WAV/OPB2WAV.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | 23 | 24 | Header Files 25 | 26 | 27 | -------------------------------------------------------------------------------- /OPB2WAV/doom.opb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Enichan/OPBinaryLib/4334ec39c16c4d06d2cda529bc73b000d727f553/OPB2WAV/doom.opb -------------------------------------------------------------------------------- /OPB2WAV/opb2wav.c: -------------------------------------------------------------------------------- 1 | /* 2 | // MIT License 3 | // 4 | // Copyright (c) 2023 Eniko Fox/Emma Maassen 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | */ 24 | #ifdef _WIN32 25 | #define _CRT_SECURE_NO_DEPRECATE 26 | #define strdup _strdup 27 | #endif 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include "..\opblib.h" 34 | 35 | #define OPL_IMPLEMENTATION 36 | #include "opl.h" 37 | 38 | #define SAMPLE_RATE 44100 39 | 40 | // OPL methods, these will depend on your OPL emulator. Here we use dos-like's opl.h implementation: 41 | // https://github.com/mattiasgustavsson/dos-like/blob/bcdec4259db66c764dbf02f16e2bc40198924091/source/libs/opl.h 42 | static opl_t* OPL_Init(void) { 43 | return opl_create(); 44 | } 45 | 46 | static void OPL_Render(void* chip, short* buffer, int samplePairs, float volume) { 47 | if (samplePairs <= 0) { 48 | return; 49 | } 50 | opl_render((opl_t*)chip, buffer, samplePairs, volume); 51 | } 52 | 53 | static void OPL_Write(void* chip, int count, uint16_t* regs, uint8_t* data) { 54 | opl_write((opl_t*)chip, count, regs, data); 55 | } 56 | 57 | // sets registers so all channels should stop producing sound 58 | static void OPL_Clear(void* chip) { 59 | uint16_t regs[0x54]; 60 | uint8_t data[0x54]; 61 | 62 | for (int j = 0, k = 0; j < 0x200; j += 0x100) { 63 | for (int i = 0; i < 0x16; i++, k++) { 64 | regs[k] = 0x40 + i + j; 65 | data[k] = 0xFF; 66 | } 67 | } 68 | 69 | OPL_Write(chip, 0x16 * 2, regs, data); 70 | 71 | for (int j = 0, k = 0; j < 0x200; j += 0x100) { 72 | for (int i = 0; i < 9; i++, k++) { 73 | regs[k] = 0xB0 + i + j; 74 | data[k] = 0; 75 | } 76 | } 77 | 78 | OPL_Write(chip, 18, regs, data); 79 | } 80 | 81 | // CommandStream is a simple dynamic array which doubles in size when it runs out of capacity 82 | typedef struct CommandStream { 83 | size_t Count; 84 | size_t Capacity; 85 | OPB_Command* Stream; 86 | } CommandStream; 87 | 88 | static inline int CommandStream_AdjustCapacity(CommandStream* cmds, size_t count) { 89 | if (count >= cmds->Capacity) { 90 | size_t newCapacity = cmds->Capacity < 16 ? 16 : cmds->Capacity; 91 | while (count >= newCapacity) { 92 | newCapacity *= 2; // double until capacity is greater-equal to number of items 93 | } 94 | 95 | OPB_Command* newStream = calloc(newCapacity, sizeof(OPB_Command)); 96 | if (newStream == NULL) { 97 | printf("Out of memory in ReceiveOpbBuffer\n"); 98 | return -1; // error 99 | } 100 | 101 | if (cmds->Stream != NULL) { 102 | // copy previous item over to new buffer 103 | memcpy(newStream, cmds->Stream, sizeof(OPB_Command) * cmds->Count); 104 | // release old buffer 105 | free(cmds->Stream); 106 | } 107 | 108 | // set new buffer and capacity 109 | cmds->Stream = newStream; 110 | cmds->Capacity = newCapacity; 111 | } 112 | return 0; // success 113 | } 114 | 115 | // used to get the exe's name when printing usage directions 116 | void GetFilename(char* path, char* result, size_t maxLen) { 117 | int lastSlash = -1; 118 | int i = 0; 119 | while (path[i] != '\0') { 120 | if (path[i] == '/' || path[i] == '\\') lastSlash = i; 121 | i++; 122 | } 123 | int pathLen = i; 124 | 125 | if (lastSlash >= 0) strncpy(result, path + lastSlash + 1, (size_t)(maxLen - 1)); 126 | else strncpy(result, path, (size_t)(maxLen - 1)); 127 | result[maxLen - 1] = '\0'; 128 | } 129 | 130 | // shoves OPB_Commands from OPB_FileToOpl into our dynamic array defined by CommandStream 131 | int ReceiveOpbBuffer(OPB_Command* commandStream, size_t commandCount, void* context) { 132 | CommandStream* cmds = (CommandStream*)context; 133 | 134 | // increase capacity if necessary 135 | if (CommandStream_AdjustCapacity(cmds, cmds->Count + commandCount)) { 136 | return -1; // out of memory 137 | } 138 | 139 | // add items 140 | for (size_t i = 0; i < commandCount; i++) { 141 | cmds->Stream[cmds->Count++] = commandStream[i]; 142 | } 143 | 144 | return 0; 145 | } 146 | 147 | // some methods that make writing to file cleaner 148 | static void WriteError() { 149 | printf("File write error"); 150 | exit(EXIT_FAILURE); 151 | } 152 | 153 | static inline void WriteChars(FILE* file, const char* value, int count) { 154 | if (fwrite(value, sizeof(char), count, file) != count) WriteError(); 155 | } 156 | 157 | static inline void WriteShorts(FILE* file, const short* value, int count) { 158 | if (fwrite(value, sizeof(short), count, file) != count) WriteError(); 159 | } 160 | 161 | static inline void WriteUInt32(FILE* file, const uint32_t value) { 162 | if (fwrite(&value, sizeof(uint32_t), 1, file) != 1) WriteError(); 163 | } 164 | 165 | static inline void WriteUInt16(FILE* file, const uint16_t value) { 166 | if (fwrite(&value, sizeof(uint16_t), 1, file) != 1) WriteError(); 167 | } 168 | 169 | // this is a buffer that holds the audio samples generated by the OPL emulator 170 | // our OPL sound sample buffer should hold 1 second (so equal to sample rate) of audio 171 | // but OPL3 is stereo so we need twice as many actual samples as sample pairs 172 | #define MAX_SAMPLES 44100 173 | short buffer[MAX_SAMPLES * 2]; 174 | 175 | // logger 176 | static void Logger(const char* s) { 177 | printf(s); 178 | } 179 | 180 | int main(int argc, char* argv[]) { 181 | if (argc < 3) { 182 | char* path = argv[0]; 183 | char filename[128]; 184 | GetFilename(path, filename, 128); 185 | 186 | printf("Usage: %s \n", filename); 187 | exit(EXIT_FAILURE); 188 | } 189 | 190 | // set logger 191 | OPB_Log = Logger; 192 | 193 | CommandStream commands = { 0 }; 194 | 195 | // unpack OPB file into OPL3 command stream 196 | printf("Unpacking %s\n", argv[1]); 197 | 198 | int error; 199 | if ((error = OPB_FileToOpl(argv[1], ReceiveOpbBuffer, &commands)) != 0) { 200 | printf("Error converting OPB file: %s\n", OPB_GetErrorMessage(error)); 201 | exit(EXIT_FAILURE); 202 | } 203 | 204 | // open wav file and write header (write end offset and data length after) 205 | printf("Writing %s\n", argv[2]); 206 | FILE* fout = fopen(argv[2], "wb"); 207 | WriteChars(fout, "RIFF", 4); 208 | WriteUInt32(fout, 0); // file end offset (data length + 36) 209 | WriteChars(fout, "WAVE", 4); 210 | WriteChars(fout, "fmt ", 4); 211 | WriteUInt32(fout, 16); 212 | WriteUInt16(fout, 1); // WAVE_FORMAT_PCM 213 | WriteUInt16(fout, 2); // channel 1=mono, 2=stero 214 | WriteUInt32(fout, SAMPLE_RATE); 215 | WriteUInt32(fout, SAMPLE_RATE * 2 * (16 / 8)); // bytes/sec 216 | WriteUInt16(fout, 2 * (16 / 8)); // block size 217 | WriteUInt16(fout, 16); // bits per sample 218 | WriteChars(fout, "data", 4); 219 | WriteUInt32(fout, 0); // data length 220 | 221 | // initialize OPL emulator and start processing commands/generating audio! 222 | printf("Initializing OPL emulator\n"); 223 | opl_t* opl = OPL_Init(); 224 | double time = 0; 225 | 226 | printf("Processing OPL command stream and writing audio samples\n"); 227 | for (size_t i = 0; i < commands.Count; i++) { 228 | OPB_Command cmd = commands.Stream[i]; 229 | 230 | if (cmd.Time > time) { 231 | // time has advanced, generate audio samples before sending this command to the OPL emulator 232 | double elapsed = cmd.Time - time; 233 | time = cmd.Time; 234 | 235 | // number of sample pairs to generate depends on sample rate 236 | int samples = (int)(elapsed * SAMPLE_RATE); 237 | while (samples > 0) { 238 | int count = samples <= MAX_SAMPLES ? samples : MAX_SAMPLES; 239 | OPL_Render(opl, buffer, count, 0.95f); // 0.95 to prevent clipping 240 | samples -= count; 241 | 242 | // write out all our sample pairs, this is count * 2 because of the number of channels 243 | WriteShorts(fout, buffer, count * 2); 244 | } 245 | } 246 | 247 | // send command to OPL emulator 248 | OPL_Write(opl, 1, &cmd.Addr, &cmd.Data); 249 | } 250 | 251 | size_t filelen = ftell(fout); 252 | 253 | // set wav header file end offset (which is file length - 8) 254 | fseek(fout, 4, SEEK_SET); 255 | WriteUInt32(fout, (uint32_t)(filelen - 8)); 256 | 257 | // set wav header data size (which is file length - 44, which is the size of the header) 258 | fseek(fout, 40, SEEK_SET); 259 | WriteUInt32(fout, (uint32_t)(filelen - 44)); 260 | 261 | // done! 262 | fclose(fout); 263 | printf("Done!\n"); 264 | 265 | // clean up 266 | free(opl); 267 | opl = NULL; 268 | } -------------------------------------------------------------------------------- /OPBinaryLib.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31313.79 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OPBinaryLib", "OPBinaryLib.vcxproj", "{272E3EC8-1734-41DB-B225-B5ADAF95FDB9}" 7 | EndProject 8 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DumpOPL", "DumpOPL\DumpOPL.vcxproj", "{F2C67507-A331-419A-B5A2-E91B1EB6002E}" 9 | EndProject 10 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OPB2WAV", "OPB2WAV\OPB2WAV.vcxproj", "{D73A35AD-A776-463E-9E72-EF37AD628208}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|x64 = Debug|x64 15 | Debug|x86 = Debug|x86 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {272E3EC8-1734-41DB-B225-B5ADAF95FDB9}.Debug|x64.ActiveCfg = Debug|x64 21 | {272E3EC8-1734-41DB-B225-B5ADAF95FDB9}.Debug|x64.Build.0 = Debug|x64 22 | {272E3EC8-1734-41DB-B225-B5ADAF95FDB9}.Debug|x86.ActiveCfg = Debug|Win32 23 | {272E3EC8-1734-41DB-B225-B5ADAF95FDB9}.Debug|x86.Build.0 = Debug|Win32 24 | {272E3EC8-1734-41DB-B225-B5ADAF95FDB9}.Release|x64.ActiveCfg = Release|x64 25 | {272E3EC8-1734-41DB-B225-B5ADAF95FDB9}.Release|x64.Build.0 = Release|x64 26 | {272E3EC8-1734-41DB-B225-B5ADAF95FDB9}.Release|x86.ActiveCfg = Release|Win32 27 | {272E3EC8-1734-41DB-B225-B5ADAF95FDB9}.Release|x86.Build.0 = Release|Win32 28 | {F2C67507-A331-419A-B5A2-E91B1EB6002E}.Debug|x64.ActiveCfg = Debug|x64 29 | {F2C67507-A331-419A-B5A2-E91B1EB6002E}.Debug|x64.Build.0 = Debug|x64 30 | {F2C67507-A331-419A-B5A2-E91B1EB6002E}.Debug|x86.ActiveCfg = Debug|Win32 31 | {F2C67507-A331-419A-B5A2-E91B1EB6002E}.Debug|x86.Build.0 = Debug|Win32 32 | {F2C67507-A331-419A-B5A2-E91B1EB6002E}.Release|x64.ActiveCfg = Release|x64 33 | {F2C67507-A331-419A-B5A2-E91B1EB6002E}.Release|x64.Build.0 = Release|x64 34 | {F2C67507-A331-419A-B5A2-E91B1EB6002E}.Release|x86.ActiveCfg = Release|Win32 35 | {F2C67507-A331-419A-B5A2-E91B1EB6002E}.Release|x86.Build.0 = Release|Win32 36 | {D73A35AD-A776-463E-9E72-EF37AD628208}.Debug|x64.ActiveCfg = Debug|x64 37 | {D73A35AD-A776-463E-9E72-EF37AD628208}.Debug|x64.Build.0 = Debug|x64 38 | {D73A35AD-A776-463E-9E72-EF37AD628208}.Debug|x86.ActiveCfg = Debug|Win32 39 | {D73A35AD-A776-463E-9E72-EF37AD628208}.Debug|x86.Build.0 = Debug|Win32 40 | {D73A35AD-A776-463E-9E72-EF37AD628208}.Release|x64.ActiveCfg = Release|x64 41 | {D73A35AD-A776-463E-9E72-EF37AD628208}.Release|x64.Build.0 = Release|x64 42 | {D73A35AD-A776-463E-9E72-EF37AD628208}.Release|x86.ActiveCfg = Release|Win32 43 | {D73A35AD-A776-463E-9E72-EF37AD628208}.Release|x86.Build.0 = Release|Win32 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {B44C12DE-B88A-4A2A-8BC6-131A609930CC} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /OPBinaryLib.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | Win32Proj 24 | {272e3ec8-1734-41db-b225-b5adaf95fdb9} 25 | OPBinaryLib 26 | 10.0 27 | 28 | 29 | 30 | StaticLibrary 31 | true 32 | v142 33 | Unicode 34 | 35 | 36 | StaticLibrary 37 | false 38 | v142 39 | true 40 | Unicode 41 | 42 | 43 | StaticLibrary 44 | true 45 | v142 46 | Unicode 47 | 48 | 49 | StaticLibrary 50 | false 51 | v142 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | false 78 | 79 | 80 | true 81 | 82 | 83 | false 84 | 85 | 86 | 87 | Level3 88 | true 89 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 90 | true 91 | 92 | 93 | Console 94 | true 95 | 96 | 97 | 98 | 99 | Level3 100 | true 101 | true 102 | true 103 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 104 | true 105 | 106 | 107 | Console 108 | true 109 | true 110 | true 111 | 112 | 113 | 114 | 115 | Level3 116 | true 117 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 118 | true 119 | 120 | 121 | Console 122 | true 123 | 124 | 125 | 126 | 127 | Level3 128 | true 129 | true 130 | true 131 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 132 | true 133 | 134 | 135 | Console 136 | true 137 | true 138 | true 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /OPBinaryLib.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | 23 | 24 | Header Files 25 | 26 | 27 | -------------------------------------------------------------------------------- /OPBinarySharp/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eniko Fox/Emma Maassen 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 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBCommand.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Eniko Fox/Emma Maassen 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 | using System; 23 | using System.Collections.Generic; 24 | using System.Globalization; 25 | using System.Linq; 26 | using System.Text; 27 | using System.Threading.Tasks; 28 | 29 | namespace OPBinarySharp { 30 | public struct OPBCommand { 31 | public ushort Addr; 32 | public byte Data; 33 | public TimeSpan Time; 34 | 35 | public OPBCommand(ushort addr, byte data, TimeSpan time) { 36 | Addr = addr; 37 | Data = data; 38 | Time = time; 39 | } 40 | 41 | public override string ToString() { 42 | return string.Format("OPBCommand(0x{0}, 0x{1}, {2})", 43 | Addr.ToString("X3", CultureInfo.InvariantCulture), 44 | Data.ToString("X2", CultureInfo.InvariantCulture), 45 | Time.ToString() 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBException.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Eniko Fox/Emma Maassen 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 | using System; 23 | using System.Collections.Generic; 24 | using System.Linq; 25 | using System.Text; 26 | using System.Threading.Tasks; 27 | 28 | namespace OPBinarySharp { 29 | [Serializable] 30 | public class OPBException : Exception { 31 | public OPBException() { } 32 | public OPBException(string message) : base(message) { } 33 | public OPBException(string message, Exception inner) : base(message, inner) { } 34 | protected OPBException( 35 | System.Runtime.Serialization.SerializationInfo info, 36 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBFormat.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Eniko Fox/Emma Maassen 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 | using System; 23 | using System.Collections.Generic; 24 | using System.Linq; 25 | using System.Text; 26 | using System.Threading.Tasks; 27 | 28 | namespace OPBinarySharp { 29 | public enum OPBFormat { 30 | Default, 31 | Raw, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBSharpTest/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBSharpTest/OPBSharpTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {2025DF1D-35AB-44EE-99BF-84705A74F268} 8 | Exe 9 | OPBSharpTest 10 | OPBSharpTest 11 | v4.7.2 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {61a6a5a2-4cfc-41fe-9fe2-05bd0ea3def7} 55 | OPBinarySharp 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBSharpTest/Program.cs: -------------------------------------------------------------------------------- 1 | using OPBinarySharp; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace OPBSharpTest { 10 | class Program { 11 | static void Main() { 12 | OPB.LogHandler = new LogHandler((s) => Console.Write(s)); 13 | 14 | var testFile = "doom.vgm"; 15 | var vgmCmds = VGM.Parse(testFile); 16 | OPB.OplToFile(OPBFormat.Default, vgmCmds, Path.GetFileNameWithoutExtension(testFile) + ".opb"); 17 | 18 | //var fileIn = "doom.opb"; 19 | //var fileOut = "doom-out.opb"; 20 | 21 | //var commands = OPB.FileToOpl(fileIn); 22 | //OPB.OplToFile(OPBFormat.Default, commands, fileOut); 23 | //var commands2 = OPB.FileToOpl(fileOut); 24 | 25 | //for (int i = 0; i < commands.Count; i++) { 26 | // if (commands[i].Addr != commands2[i].Addr || commands[i].Data != commands2[i].Data || Math.Abs(commands[i].Time.TotalMilliseconds - commands2[i].Time.TotalMilliseconds) > 0.00001) { 27 | // throw new Exception(); 28 | // } 29 | //} 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBSharpTest/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("OPBSharpTest")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("OPBSharpTest")] 13 | [assembly: AssemblyCopyright("Copyright © 2023")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("2025df1d-35ab-44ee-99bf-84705a74f268")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBSharpTest/doom.vgm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Enichan/OPBinaryLib/4334ec39c16c4d06d2cda529bc73b000d727f553/OPBinarySharp/OPBSharpTest/doom.vgm -------------------------------------------------------------------------------- /OPBinarySharp/OPBinarySharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {61A6A5A2-4CFC-41FE-9FE2-05BD0EA3DEF7} 8 | Library 9 | Properties 10 | OPBinarySharp 11 | OPBinarySharp 12 | v4.7.2 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | TRACE;DEBUG 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /OPBinarySharp/OPBinarySharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.32802.440 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OPBinarySharp", "OPBinarySharp.csproj", "{61A6A5A2-4CFC-41FE-9FE2-05BD0EA3DEF7}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OPBSharpTest", "OPBSharpTest\OPBSharpTest.csproj", "{2025DF1D-35AB-44EE-99BF-84705A74F268}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {61A6A5A2-4CFC-41FE-9FE2-05BD0EA3DEF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {61A6A5A2-4CFC-41FE-9FE2-05BD0EA3DEF7}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {61A6A5A2-4CFC-41FE-9FE2-05BD0EA3DEF7}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {61A6A5A2-4CFC-41FE-9FE2-05BD0EA3DEF7}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2025DF1D-35AB-44EE-99BF-84705A74F268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2025DF1D-35AB-44EE-99BF-84705A74F268}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2025DF1D-35AB-44EE-99BF-84705A74F268}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2025DF1D-35AB-44EE-99BF-84705A74F268}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {DB093CED-C603-489D-A58B-A5954134DCF5} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /OPBinarySharp/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("OPBinarySharp")] 9 | [assembly: AssemblyDescription("OPL3 music format reading/writing library")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Eniko Fox")] 12 | [assembly: AssemblyProduct("OPBinarySharp")] 13 | [assembly: AssemblyCopyright("Copyright © 2023")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("61a6a5a2-4cfc-41fe-9fe2-05bd0ea3def7")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /OPBinarySharp/README.md: -------------------------------------------------------------------------------- 1 | # OPBinarySharp 2 | 3 | OPBinarySharp is a C# port of [OPBinaryLib](https://github.com/Enichan/OPBinaryLib). It uses only safe, 100% managed code targeting .NET framework 4.7.2 (4.5) 4 | 5 | ```csharp 6 | // load OPB file 7 | var commands = OPBinarySharp.OPB.FileToOpl("mysong.opb"); 8 | 9 | // write OPB file 10 | OPBinarySharp.OPB.OplToFile(OPBFormat.Default, commands, "mysong-clone.opb"); 11 | ``` 12 | 13 | # OPBinaryLib 14 | 15 | OPBinaryLib is a C/C++ library for converting a stream of OPL FM synth chip commands to the OPB music format. A fully managed C# port called [OPBinarySharp](https://github.com/Enichan/OPBinaryLib/tree/main/OPBinarySharp) is also included in this repository. 16 | 17 | The OPB music format is a format that stores commands for the Yamaha OPL3 chip (Yamaha YMF262) which was used by DOS soundcards and games. It aims to reduce the size of files storing OPL command streams to close to MIDI (usually less than 2x the size of MIDI) while still being fairly straightforward to parse. OPB files tend to be pretty similar in size to gzip compressed VGM (VGZ) files, but don't require a complex decompression algorithm to read, and can be compressed to be far smaller than VGZ files. 18 | 19 | Anyone is encouraged to use the format for their own purposes, with or without the provided C code library. 20 | 21 | ## Generating OPB files from MIDI 22 | 23 | Currently the best way to generate OPB files is to use the [CaptureOPL utility](https://github.com/Enichan/libADLMIDI/releases) to generate OPB files from MIDI, MUS, or XMI files. This utility uses a fork of libADLMIDI (original [here](https://github.com/Wohlstand/libADLMIDI)) to capture the OPL output from libADLMIDI's playback and encodes the stream of OPL commands as an OPB music file. 24 | 25 | ## How to compose OPB files 26 | 27 | There are two ways to compose music to be turned into OPB files: 28 | 29 | ### MIDI 30 | 31 | Use the [ADLplug](https://github.com/jpcima/ADLplug/releases) VST with your DAW to compose your MIDI with the same sound banks that are available in the [CaptureOPL utility](https://github.com/Enichan/libADLMIDI/releases). Once you're happy with your music use the utility to convert your MIDI to OPB. 32 | 33 | ### Trackers 34 | 35 | Any OPL3 capable tracker that outputs VGM files will work, such as [Furnace](https://github.com/tildearrow/furnace/releases). Compose your OPL3 song, export to VGM, then convert to OPB. 36 | 37 | ## How to program with OPB files 38 | 39 | - Use opblib.c/opblib.h to convert an OPB file back to a stream of timestamped OPL3 chip commands 40 | - Send chip commands to one of the many available OPL chip emulators[[1]](https://github.com/aaronsgiles/ymfm)[[2]](https://github.com/nukeykt/Nuked-OPL3)[[3]](https://github.com/rofl0r/woody-opl)[[4]](https://github.com/gtaylormb/opl3_fpga/blob/master/docs/OPL3.java)[[5]](https://github.com/mattiasgustavsson/dos-like/blob/main/source/libs/opl.h) and generate samples 41 | - Use a library like [FNA](https://fna-xna.github.io/), [MonoGame](https://www.monogame.net/), or [raylib](https://www.raylib.com/) which allow you to submit buffers of audio samples to play the sound (DynamicSoundEffectInstance in FNA/MonoGame, UpdateAudioStream in raylib) 42 | 43 | ## Basic library usage 44 | 45 | Store your OPL commands as a contiguous array of `OPB_Command` (including the time in seconds for each command), then call `OPB_OplToFile` to write an OPB file. 46 | 47 | ```c 48 | OPB_OplToFile(OPB_Format_Default, commandArray, commandCount, "out.opb"); 49 | ``` 50 | 51 | This'll return 0 on success or one of the error codes in opblib.h otherwise. 52 | 53 | To turn OPB data back into a stream of `OPB_Command` values create a function to receive buffered stream data and use `OPB_FileToOpl`: 54 | 55 | ```c 56 | int ReceiveOpbBuffer(OPB_Command* commandStream, size_t commandCount, void* context) { 57 | for (int i = 0; i < commandCount; i++) { 58 | // do things here with commandStream[i] 59 | } 60 | return 0; 61 | } 62 | 63 | int main(int argc, char* argv[]) { 64 | OPB_FileToOpl("in.opb", ReceiveOpbBuffer, NULL); 65 | } 66 | ``` 67 | 68 | Optionally you can pass in a `void*` pointer to user data that will be sent to the receiver function as the `context` argument. 69 | 70 | Set `OPB_Log` to a logging implementation to get logging. 71 | 72 | The OPB2WAV converter serves as a fully documented sample for reading an OPB file, generating audio via an OPL chip emulator, and storing that as a WAV file. 73 | 74 | ## How does OPBinaryLib reduce size 75 | 76 | There are two main approaches to reducing the size of a stream of OPL3 commands that OPBinaryLib uses. 77 | 78 | The first is a simple change that can be made because commands are stored in chunks, with each chunk of commands taking place at the same time. OPL commands typically store the register to write to as a 16-bit integer, with 0x00XX for the first 9 channels and 0x01XX for the final 9 channels. OPB instead stores a count of low (0x00XX) and high (0x01XX) register commands with each chunk, which means that each individual command only needs to store the low 8-bits of its register, which saves 1 byte per command, which means commands are 1/3rd the size. 79 | 80 | The second approach involves a bank of "instruments". An instrument in OPB terms is a set of 9 properties: feedback/connection, and the characteristic, attack/decay, sustain/release, and wave select properties for both the modulator and carrier. Setting all of these properties using regular commands would take 18 bytes of storage. For each chunk of commands OPBinaryLib detects commands to set these properties for a single channel, and aggregates them into a single instrument which is stored near the start of an OPB file. Then to set the instrument for a channel, OPBinaryLib encodes a single special command (special commands use the unused 0xD0 through 0xDF registers) which is between 4 and 9 bytes long that specifies the instrument to use and which of its properties to set. 81 | 82 | Because only a subset of properties for an instrument can be set, a partial match can still use an existing instrument. Additionally, because setting an instrument's properties will often be accompanied by carrier and modulator levels (aka volume) these can optionally be encoded in the command, which saves additional bytes. Finally, because setting these properties often comes before a note command, there's another special command which sets the instrument and takes the note and frequency data, saving another 2 bytes. 83 | 84 | There are some additional, though somewhat less potent strategies employed to reduce size. One is the "combined note" special command which combines the note and frequency commands into one 3 byte command, saving 1 byte over performing them separately. Finally values larger than a single byte (elapsed time, instrument indices, command counts) outside the header are encoded using a variable length integer, so low values (below 128) need only 1 byte of storage instead of 2 or 4. 85 | 86 | ## Projects that support OPB files 87 | 88 | - [dos-like](https://github.com/mattiasgustavsson/dos-like) (C): dos-like is a programming library/framework, kind of like a tiny game engine, for writing games and programs with a similar feel to MS-DOS productions from the early 90s -------------------------------------------------------------------------------- /OPBinarySharp/VGM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | 6 | namespace OPBinarySharp { 7 | public class VGM { 8 | private const int vgmSampleRate = 44100; 9 | 10 | public static List Parse(string filePath) { 11 | using (var stream = File.OpenRead(filePath)) { 12 | return Parse(stream); 13 | } 14 | } 15 | 16 | public static List Parse(Stream stream) { 17 | using (var reader = new BinaryReader(stream)) { 18 | return Parse(reader); 19 | } 20 | } 21 | 22 | public static List Parse(BinaryReader reader) { 23 | if (reader.ReadByte() != 'V' || reader.ReadByte() != 'g' || reader.ReadByte() != 'm' || reader.ReadByte() != ' ') { 24 | throw new OPBException("Not a VGM file"); 25 | } 26 | 27 | var length = reader.BaseStream.Position + reader.ReadUInt32(); 28 | var version = reader.ReadUInt32(); 29 | if (version < 0x151) { 30 | // YMF262 support was added in 1.51 31 | throw new OPBException("Unsupported VGM version"); 32 | } 33 | 34 | reader.ReadUInt32(); // SN76489 clock 35 | reader.ReadUInt32(); // YM2413 clock 36 | 37 | var gd3Offset = reader.BaseStream.Position + reader.ReadUInt32(); // Offset to description tag 38 | var totalSamples = reader.ReadUInt32(); // Total of all wait values in the file 39 | var loopOffset = reader.BaseStream.Position + reader.ReadUInt32(); // Relative offset to loop point, or 0 if no loop 40 | var loopSamples = reader.ReadUInt32(); // Number of samples in one loop, or 0 if there is no loop. Total of all wait values between the loop point and the end of the file 41 | 42 | reader.ReadUInt32(); // "Rate" of recording in Hz, used for rate scaling on playback. It is typically 50 for PAL systems and 60 for NTSC systems 43 | reader.ReadUInt32(); // SN FB, SNW, SF 44 | reader.ReadUInt32(); // YM2612 clock 45 | reader.ReadUInt32(); // YM2151 clock 46 | 47 | var dataOffset = reader.BaseStream.Position + reader.ReadUInt32(); 48 | 49 | // A VGM file parser should be aware that some tools may write invalid loop offsets, resulting in out-of-range file offsets or 50 | // 0-sample loops and treat those as "no loop". (and possibly throw a warning) 51 | if (loopOffset < dataOffset || loopOffset >= length) { 52 | loopOffset = 0; 53 | loopSamples = 0; 54 | } 55 | 56 | reader.ReadUInt32(); // Sega PCM clock 57 | reader.ReadUInt32(); // SPCM Interface 58 | 59 | reader.BaseStream.Seek(0x50, SeekOrigin.Begin); 60 | 61 | if (length < 0x60) { 62 | throw new OPBException("Not a valid OPL3 VGM file"); 63 | } 64 | 65 | var opl2Clock = reader.ReadUInt32(); // YM3812 clock 66 | var opl1Clock = reader.ReadUInt32(); // YM3526 clock 67 | var msxClock = reader.ReadUInt32(); // Y8950 clock (MSX-AUDIO, OPL with ADPCM) 68 | var opl3Clock = reader.ReadUInt32(); // YMF262 clock 69 | 70 | var clock = Math.Max(msxClock, Math.Max(Math.Max(opl1Clock, opl2Clock), opl3Clock)); 71 | if (clock == 0) { 72 | throw new OPBException("Not a valid OPL3-compatible VGM file"); 73 | } 74 | 75 | float volume = 1; 76 | 77 | if (dataOffset >= 0x80) { 78 | int volumeMod = reader.ReadByte(); 79 | if (volumeMod > 0xC0) { 80 | volumeMod = -64 + volumeMod - 0xC0; 81 | } 82 | if (volumeMod == -63) { 83 | volumeMod = -64; 84 | } 85 | volume = (float)Math.Pow(2, volumeMod / 0x20); 86 | } 87 | 88 | reader.BaseStream.Seek(dataOffset, SeekOrigin.Begin); 89 | 90 | int sample = 0; 91 | bool end = false; 92 | var commands = new List(); 93 | 94 | while (!end && reader.BaseStream.Position < length) { 95 | var cmd = (VGMCommand)reader.ReadByte(); 96 | 97 | switch (cmd) { 98 | default: 99 | throw new OPBException(string.Format("Unsupported VGM command 0x{0}", ((int)cmd).ToString("X2", CultureInfo.InvariantCulture))); 100 | 101 | case VGMCommand.EndOfData: 102 | end = true; 103 | break; 104 | 105 | case VGMCommand.Wait: 106 | sample += reader.ReadUInt16(); 107 | break; 108 | 109 | case VGMCommand.Wait50: 110 | sample += 882; // 1/50th of a second 111 | break; 112 | case VGMCommand.Wait60: 113 | sample += 735; // 1/60th of a second 114 | break; 115 | 116 | case VGMCommand.Wait1: 117 | case VGMCommand.Wait2: 118 | case VGMCommand.Wait3: 119 | case VGMCommand.Wait4: 120 | case VGMCommand.Wait5: 121 | case VGMCommand.Wait6: 122 | case VGMCommand.Wait7: 123 | case VGMCommand.Wait8: 124 | case VGMCommand.Wait9: 125 | case VGMCommand.Wait10: 126 | case VGMCommand.Wait11: 127 | case VGMCommand.Wait12: 128 | case VGMCommand.Wait13: 129 | case VGMCommand.Wait14: 130 | case VGMCommand.Wait15: 131 | case VGMCommand.Wait16: 132 | sample += 1 + cmd - VGMCommand.Wait1; 133 | break; 134 | 135 | case VGMCommand.YM3812: 136 | case VGMCommand.YM3526: 137 | case VGMCommand.Y8950: 138 | case VGMCommand.YMF262Port0: 139 | case VGMCommand.YMF262Port1: { 140 | var time = TimeSpan.FromSeconds(sample / (double)vgmSampleRate); 141 | var addr = reader.ReadByte() + (cmd < VGMCommand.YMF262Port0 ? 0 : cmd - VGMCommand.YMF262Port0) * 0x100; 142 | var data = reader.ReadByte(); 143 | commands.Add(new OPBCommand((ushort)addr, (byte)data, time)); 144 | break; 145 | } 146 | 147 | // data blocks are used by Y8950 chip but aren't OPL compatible so just skip em 148 | case VGMCommand.DataBlock: { 149 | reader.ReadByte(); // 0x66 compatibility command 150 | var size = reader.ReadUInt32(); 151 | reader.BaseStream.Seek(size, SeekOrigin.Current); 152 | break; 153 | } 154 | 155 | // writes data blocks to various places, ignored 156 | case VGMCommand.PCMRamWrite: 157 | reader.BaseStream.Seek(11, SeekOrigin.Current); 158 | break; 159 | case VGMCommand.DACStreamSetup: 160 | reader.BaseStream.Seek(4, SeekOrigin.Current); 161 | break; 162 | case VGMCommand.DACStreamSetData: 163 | reader.BaseStream.Seek(4, SeekOrigin.Current); 164 | break; 165 | case VGMCommand.DACStreamFrequency: 166 | reader.BaseStream.Seek(5, SeekOrigin.Current); 167 | break; 168 | case VGMCommand.DACStreamStart: 169 | reader.BaseStream.Seek(10, SeekOrigin.Current); 170 | break; 171 | case VGMCommand.DACStreamStop: 172 | reader.BaseStream.Seek(1, SeekOrigin.Current); 173 | break; 174 | case VGMCommand.DACStreamStartFast: 175 | reader.BaseStream.Seek(4, SeekOrigin.Current); 176 | break; 177 | } 178 | } 179 | 180 | return commands; 181 | } 182 | 183 | private enum VGMCommand { 184 | YM3812 = 0x5A, 185 | YM3526 = 0x5B, 186 | Y8950 = 0x5C, 187 | YMF262Port0 = 0x5E, 188 | YMF262Port1 = 0x5F, 189 | Wait = 0x61, 190 | Wait60 = 0x62, 191 | Wait50 = 0x63, 192 | EndOfData = 0x66, 193 | DataBlock = 0x67, 194 | PCMRamWrite = 0x68, 195 | DACStreamSetup = 0x90, 196 | DACStreamSetData = 0x91, 197 | DACStreamFrequency = 0x92, 198 | DACStreamStart = 0x93, 199 | DACStreamStop = 0x94, 200 | DACStreamStartFast = 0x95, 201 | Wait1 = 0x70, 202 | Wait2 = 0x71, 203 | Wait3 = 0x72, 204 | Wait4 = 0x73, 205 | Wait5 = 0x74, 206 | Wait6 = 0x75, 207 | Wait7 = 0x76, 208 | Wait8 = 0x77, 209 | Wait9 = 0x78, 210 | Wait10 = 0x79, 211 | Wait11 = 0x7A, 212 | Wait12 = 0x7B, 213 | Wait13 = 0x7C, 214 | Wait14 = 0x7D, 215 | Wait15 = 0x7E, 216 | Wait16 = 0x7F, 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OPBinaryLib 2 | 3 | OPBinaryLib is a C/C++ library for converting a stream of OPL FM synth chip commands to the OPB music format. A fully managed C# port called [OPBinarySharp](https://github.com/Enichan/OPBinaryLib/tree/main/OPBinarySharp) is also included in this repository. 4 | 5 | The OPB music format is a format that stores commands for the Yamaha OPL3 chip (Yamaha YMF262) which was used by DOS soundcards and games. It aims to reduce the size of files storing OPL command streams to close to MIDI (usually less than 2x the size of MIDI) while still being fairly straightforward to parse. OPB files tend to be pretty similar in size to gzip compressed VGM (VGZ) files, but don't require a complex decompression algorithm to read, and can be compressed to be far smaller than VGZ files. 6 | 7 | Anyone is encouraged to use the format for their own purposes, with or without the provided C code library. 8 | 9 | ## Generating OPB files from MIDI 10 | 11 | Currently the best way to generate OPB files is to use the [CaptureOPL utility](https://github.com/Enichan/libADLMIDI/releases) to generate OPB files from MIDI, MUS, or XMI files. This utility uses a fork of libADLMIDI (original [here](https://github.com/Wohlstand/libADLMIDI)) to capture the OPL output from libADLMIDI's playback and encodes the stream of OPL commands as an OPB music file. 12 | 13 | ## How to compose OPB files 14 | 15 | There are two ways to compose music to be turned into OPB files: 16 | 17 | ### MIDI 18 | 19 | Use the [ADLplug](https://github.com/jpcima/ADLplug/releases) VST with your DAW to compose your MIDI with the same sound banks that are available in the [CaptureOPL utility](https://github.com/Enichan/libADLMIDI/releases). Once you're happy with your music use the utility to convert your MIDI to OPB. 20 | 21 | ### Trackers 22 | 23 | Any OPL3 capable tracker that outputs VGM files will work, such as [Furnace](https://github.com/tildearrow/furnace/releases). Compose your OPL3 song, export to VGM, then convert to OPB. 24 | 25 | ## How to program with OPB files 26 | 27 | - Use opblib.c/opblib.h to convert an OPB file back to a stream of timestamped OPL3 chip commands 28 | - Send chip commands to one of the many available OPL chip emulators[[1]](https://github.com/aaronsgiles/ymfm)[[2]](https://github.com/nukeykt/Nuked-OPL3)[[3]](https://github.com/rofl0r/woody-opl)[[4]](https://github.com/gtaylormb/opl3_fpga/blob/master/docs/OPL3.java)[[5]](https://github.com/mattiasgustavsson/dos-like/blob/main/source/libs/opl.h) and generate samples 29 | - Use a library like [FNA](https://fna-xna.github.io/), [MonoGame](https://www.monogame.net/), or [raylib](https://www.raylib.com/) which allow you to submit buffers of audio samples to play the sound (DynamicSoundEffectInstance in FNA/MonoGame, UpdateAudioStream in raylib) 30 | 31 | ## Basic library usage 32 | 33 | Store your OPL commands as a contiguous array of `OPB_Command` (including the time in seconds for each command), then call `OPB_OplToFile` to write an OPB file. 34 | 35 | ```c 36 | OPB_OplToFile(OPB_Format_Default, commandArray, commandCount, "out.opb"); 37 | ``` 38 | 39 | This'll return 0 on success or one of the error codes in opblib.h otherwise. 40 | 41 | To turn OPB data back into a stream of `OPB_Command` values create a function to receive buffered stream data and use `OPB_FileToOpl`: 42 | 43 | ```c 44 | int ReceiveOpbBuffer(OPB_Command* commandStream, size_t commandCount, void* context) { 45 | for (int i = 0; i < commandCount; i++) { 46 | // do things here with commandStream[i] 47 | } 48 | return 0; 49 | } 50 | 51 | int main(int argc, char* argv[]) { 52 | OPB_FileToOpl("in.opb", ReceiveOpbBuffer, NULL); 53 | } 54 | ``` 55 | 56 | Optionally you can pass in a `void*` pointer to user data that will be sent to the receiver function as the `context` argument. 57 | 58 | Set `OPB_Log` to a logging implementation to get logging. 59 | 60 | The OPB2WAV converter serves as a fully documented sample for reading an OPB file, generating audio via an OPL chip emulator, and storing that as a WAV file. 61 | 62 | ## How does OPBinaryLib reduce size 63 | 64 | There are two main approaches to reducing the size of a stream of OPL3 commands that OPBinaryLib uses. 65 | 66 | The first is a simple change that can be made because commands are stored in chunks, with each chunk of commands taking place at the same time. OPL commands typically store the register to write to as a 16-bit integer, with 0x00XX for the first 9 channels and 0x01XX for the final 9 channels. OPB instead stores a count of low (0x00XX) and high (0x01XX) register commands with each chunk, which means that each individual command only needs to store the low 8-bits of its register, which saves 1 byte per command, which means commands are 1/3rd the size. 67 | 68 | The second approach involves a bank of "instruments". An instrument in OPB terms is a set of 9 properties: feedback/connection, and the characteristic, attack/decay, sustain/release, and wave select properties for both the modulator and carrier. Setting all of these properties using regular commands would take 18 bytes of storage. For each chunk of commands OPBinaryLib detects commands to set these properties for a single channel, and aggregates them into a single instrument which is stored near the start of an OPB file. Then to set the instrument for a channel, OPBinaryLib encodes a single special command (special commands use the unused 0xD0 through 0xDF registers) which is between 4 and 9 bytes long that specifies the instrument to use and which of its properties to set. 69 | 70 | Because only a subset of properties for an instrument can be set, a partial match can still use an existing instrument. Additionally, because setting an instrument's properties will often be accompanied by carrier and modulator levels (aka volume) these can optionally be encoded in the command, which saves additional bytes. Finally, because setting these properties often comes before a note command, there's another special command which sets the instrument and takes the note and frequency data, saving another 2 bytes. 71 | 72 | There are some additional, though somewhat less potent strategies employed to reduce size. One is the "combined note" special command which combines the note and frequency commands into one 3 byte command, saving 1 byte over performing them separately. Finally values larger than a single byte (elapsed time, instrument indices, command counts) outside the header are encoded using a variable length integer, so low values (below 128) need only 1 byte of storage instead of 2 or 4. 73 | 74 | ## Projects that support OPB files 75 | 76 | - [dos-like](https://github.com/mattiasgustavsson/dos-like) (C): dos-like is a programming library/framework, kind of like a tiny game engine, for writing games and programs with a similar feel to MS-DOS productions from the early 90s -------------------------------------------------------------------------------- /opb_file_specification.txt: -------------------------------------------------------------------------------- 1 | OPB - OPL Binary Music Format 2 | Version 1 3 | 4 | Code to read and write OPB files can be found in the OPBinaryLib repository on 5 | GitHub: https://github.com/Enichan/OPBinaryLib 6 | 7 | The type uint7+ is a special type of up to 29 bits where the first 3 bytes may 8 | have the top bit set to indicate another byte of data follows them. This means 9 | this type can be between 1 and 4 bytes long and can contain 7, 14, 21, or 29 10 | bits of data. C code for reading, writing, and determining the size of an 11 | uint7+ is included at the bottom of this document. 12 | 13 | All values in this format are big endian. Except for the uint7+ type which 14 | starts with the low byte. 15 | 16 | If the format is "raw", the entire file after the first 8 bytes is made up of a 17 | stream of uint16 elapsed timestamp in milliseconds followed by uint16 OPL 18 | address register and uint8 data. 19 | 20 | For more information about OPL and its registers I recommend "Programming the 21 | AdLib/Sound Blaster FM Music Chips Version 2.0" by Jeffrey S. Lee which you can 22 | find a copy of at http://bespin.org/~qz/pc-gpe/adlib.txt. This specification 23 | reproduces the address/function and operator offset tables from his guide here 24 | for convenience: 25 | 26 | Address Function 27 | ------- ---------------------------------------------------- 28 | 01 Test LSI / Enable waveform control 29 | 02 Timer 1 data 30 | 03 Timer 2 data 31 | 04 Timer control flags 32 | 08 Speech synthesis mode / Keyboard split note select 33 | 20..35 Amp Mod / Vibrato / EG type / Key Scaling / Multiple 34 | 40..55 Key scaling level / Operator output level 35 | 60..75 Attack Rate / Decay Rate 36 | 80..95 Sustain Level / Release Rate 37 | A0..A8 Frequency (low 8 bits) 38 | B0..B8 Key On / Octave / Frequency (high 2 bits) 39 | BD AM depth / Vibrato depth / Rhythm control 40 | C0..C8 Feedback strength / Connection type 41 | E0..F5 Wave Select 42 | 43 | Channel 1 2 3 4 5 6 7 8 9 44 | Operator 1 00 01 02 08 09 0A 10 11 12 45 | Operator 2 03 04 05 0B 0C 0D 13 14 15 46 | 47 | 48 | 49 | File ID 50 | 51 | [char*5] "OPBin" 52 | [char] Version (starting at the character '1', not 0x1) 53 | [uint8] Must be 0x0 54 | 55 | 56 | Header 57 | 58 | [uint8] Format (0 = standard, 1 = raw) 59 | [uint32] Size in bytes 60 | [uint32] InstrumentCount 61 | [uint32] ChunkCount 62 | 63 | 64 | Instruments x InstrumentCount 65 | 66 | [uint8] Feedback/connection (base reg C0) 67 | 68 | Modulator 69 | 70 | [uint8] Characteristic (base reg 20) (Mult, KSR, EG, VIB and AM flags) 71 | [uint8] Attack/decay level (base reg 60) 72 | [uint8] Sustain/release level (base reg 80) 73 | [uint8] Wave select (base reg E0) 74 | 75 | Carrier 76 | 77 | [uint8] Characteristic (base reg 23) (Mult, KSR, EG, VIB and AM flags) 78 | [uint8] Attack/decay level (base reg 63) 79 | [uint8] Sustain/release level (base reg 83) 80 | [uint8] Wave select (base reg E3) 81 | 82 | 83 | Chunks x ChunkCount 84 | 85 | [uint7+] Time elapsed since last chunk (in milliseconds) 86 | [uint7+] OPL_CommandCountLo 87 | [uint7+] OPL_CommandCountHi 88 | 89 | OPL Command x OPL_CommandCountLo 90 | 91 | [uint8] Register 92 | [uint8] Data 93 | 94 | OPL Command x OPL_CommandCountHi 95 | 96 | [uint8] Register 97 | [uint8] Data 98 | 99 | 100 | 101 | OPB Command List 102 | 103 | These are marked by register addresses that are normally unused in the D0-DF 104 | range and instead of data are followed by the arguments noted here. 105 | 106 | 107 | Hex Function and arguments 108 | Val Description 109 | 110 | D0 Set instrument 111 | Arguments: uint7+ instrIndex, uint8 channelMask, uint8 mask 112 | 113 | Sets instrument (OPL registers 20, 40, 60, 80, E0, C0) properties for the 114 | specified channel according to the bits contained in the channelMask and 115 | mask arguments. For each bit set in such a way write the instrument's 116 | corresponding properties to the appropriate offset for the channel and 117 | operator (modulator = operator 1, carrier = operator 2). 118 | 119 | For example, if the lower 5 bits of channelMask encodes channel 1 and bit 120 | 2 of the mask argument is set this indicates the instrument's modulator 121 | sustain/release data should be written to the OPL chip's 2nd channel. 122 | 123 | The OPL register range for sustain/release is hex 80-95. The offset for 124 | operator 1 (modulator) for the second channel (channel 1) is 1. This 125 | means the instrument's modulator sustain/release data byte should be 126 | written to OPL register hex 80 + 1 = 81. 127 | 128 | Because levels data is not a property of instruments, when bits 5 or 6 129 | of the channelMask argument are set the following optional arguments must 130 | be read: 131 | 132 | uint8 modLevels, uint8 carLevels 133 | 134 | If bit 5 is set modLevels is read, then if bit 6 is set carLevels is read. 135 | If only bit 6 is read, read only carLevels and vice versa. 136 | 137 | For example, if the channel number is 3 (4th channel) and the 6th bit of 138 | channelMask is set (carrier levels) this means that data should be 139 | written to OPL register 40 + 0B = 4B, because the operator 2 (carrier) 140 | offset on the 4th channel is 0B. 141 | 142 | Argument descriptions: 143 | 144 | instrIndex Index of instrument in instrument table 145 | 146 | channelMask Contains the following information in its bits: 147 | 148 | 0-4 Channel for instrument (two-op melodic mode) 149 | 5 Arguments are followed by a byte describing modulator 150 | levels (OPL register 40) 151 | 6 Arguments are followed by a byte describing carrier levels 152 | (OPL register 40) 153 | 7 Set instrument feedback/connection (OPL register C0) 154 | 155 | mask Contains the following information in its bits: 156 | 157 | 0 Set instrument modulator characteristics (OPL register 20) 158 | 1 Set instrument modulator attack/decay (OPL register 60) 159 | 2 Set instrument modulator sustain/release (OPL register 80) 160 | 3 Set instrument modulator wave select (OPL register E0) 161 | 4 Set instrument carrier characteristics (OPL register 20) 162 | 5 Set instrument carrier attack/decay (OPL register 60) 163 | 6 Set instrument carrier sustain/release (OPL register 80) 164 | 7 Set instrument carrier wave select (OPL register E0) 165 | 166 | Optional arguments: 167 | 168 | modLevels Data byte describing modulator levels data (OPL register 169 | 40) if bit 5 of channelMask is set 170 | 171 | carLevels Data byte describing carrier levels data (OPL register 40) 172 | if bit 6 of channelMask is set 173 | 174 | D1 Play instrument 175 | Arguments: uint7+ instrIndex, uint8 channelMask, uint8 mask, uint8 freq, 176 | uint8 note 177 | 178 | The play instrument command functions in the same manner as the "Set 179 | instrument" command. The main difference is the inclusion of the 180 | freq and note arguments following the mask argument. These encode the 181 | frequency (OPL register A0) and note on/off (OPL register B0) data. 182 | 183 | Argument descriptions: 184 | 185 | instrIndex Index of instrument in instrument table 186 | 187 | channelMask Contains the following information in its bits: 188 | 189 | 0-4 Channel for instrument (two-op melodic mode) 190 | 5 Arguments are followed by a byte describing modulator 191 | levels (OPL register 40) 192 | 6 Arguments are followed by a byte describing carrier levels 193 | (OPL register 40) 194 | 7 Set instrument feedback/connection (OPL register C0) 195 | 196 | mask Contains the following information in its bits: 197 | 198 | 0 Set instrument modulator characteristics (OPL register 20) 199 | 1 Set instrument modulator attack/decay (OPL register 60) 200 | 2 Set instrument modulator sustain/release (OPL register 80) 201 | 3 Set instrument modulator wave select (OPL register E0) 202 | 4 Set instrument carrier characteristics (OPL register 20) 203 | 5 Set instrument carrier attack/decay (OPL register 60) 204 | 6 Set instrument carrier sustain/release (OPL register 80) 205 | 7 Set instrument carrier wave select (OPL register E0) 206 | 207 | freq Frequency data to be written to OPL register A0 208 | 209 | note Note on/off data to be written to OPL register B0 210 | 211 | Optional arguments: 212 | 213 | modLevels Data byte describing modulator levels data (OPL register 214 | 40) if bit 5 of channelMask is set 215 | 216 | carLevels Data byte describing carrier levels data (OPL register 40) 217 | if bit 6 of channelMask is set 218 | 219 | D7-DF Combined note 220 | Arguments: uint8 freq, uint8 note 221 | 222 | This command combines the data for frequency (OPL register A0) and note 223 | on (OPL register B0). The top two bits of the note data, which are unused 224 | in the OPL spec, encode the presence of optional arguments for modulator 225 | volume and carrier volume, much like the "Set instrument" command: 226 | 227 | uint8 modLevels, uint8 carLevels 228 | 229 | The channel for the combined note is the offset of this command from 230 | register D7 plus 9 channels if this command is found in the hi command 231 | stream for a chunk. It can be calculated as such: 232 | 233 | channel = (commandValue - 0xD7) + (hi ? 9 : 0) 234 | 235 | Argument descriptions: 236 | 237 | freq Frequency data to be written to OPL register A0 238 | 239 | note Note on/off data to be written to OPL register B0 240 | 241 | Optional arguments: 242 | 243 | modLevels Data byte describing modulator levels data (OPL register 244 | 40) if bit 6 of note is set 245 | 246 | carLevels Data byte describing carrier levels data (OPL register 40) 247 | if bit 7 of note is set 248 | 249 | 250 | 251 | OPB uint7+ code reference (C language) 252 | 253 | Read: 254 | 255 | // returns -1 if failed, otherwise uint7+ value 256 | int ReadUint7(FILE* file) { 257 | uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; 258 | 259 | if (fread(&b0, sizeof(uint8_t), 1, file) != 1) return -1; 260 | if (b0 >= 128) { 261 | b0 &= 0b01111111; 262 | if (fread(&b1, sizeof(uint8_t), 1, file) != 1) return -1; 263 | if (b1 >= 128) { 264 | b1 &= 0b01111111; 265 | if (fread(&b2, sizeof(uint8_t), 1, file) != 1) return -1; 266 | if (b2 >= 128) { 267 | b2 &= 0b01111111; 268 | if (fread(&b3, sizeof(uint8_t), 1, file) != 1) return -1; 269 | } 270 | } 271 | } 272 | 273 | return b0 | (b1 << 7) | (b2 << 14) | (b3 << 21); 274 | } 275 | 276 | Write: 277 | 278 | // returns -1 if failed, 0 otherwise 279 | int WriteUint7(FILE* file, uint32_t value) { 280 | if (value >= 2097152) { 281 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 282 | uint8_t b1 = ((value & 0b011111110000000) >> 7) | 0b10000000; 283 | uint8_t b2 = ((value & 0b0111111100000000000000) >> 14) | 0b10000000; 284 | uint8_t b3 = ((value & 0b11111111000000000000000000000) >> 21); 285 | if (fwrite(&b0, sizeof(uint8_t), 1, file) < 1) return -1; 286 | if (fwrite(&b1, sizeof(uint8_t), 1, file) < 1) return -1; 287 | if (fwrite(&b2, sizeof(uint8_t), 1, file) < 1) return -1; 288 | if (fwrite(&b3, sizeof(uint8_t), 1, file) < 1) return -1; 289 | } 290 | else if (value >= 16384) { 291 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 292 | uint8_t b1 = ((value & 0b011111110000000) >> 7) | 0b10000000; 293 | uint8_t b2 = (value & 0b0111111100000000000000) >> 14; 294 | if (fwrite(&b0, sizeof(uint8_t), 1, file) < 1) return -1; 295 | if (fwrite(&b1, sizeof(uint8_t), 1, file) < 1) return -1; 296 | if (fwrite(&b2, sizeof(uint8_t), 1, file) < 1) return -1; 297 | } 298 | else if (value >= 128) { 299 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 300 | uint8_t b1 = (value & 0b011111110000000) >> 7; 301 | if (fwrite(&b0, sizeof(uint8_t), 1, file) < 1) return -1; 302 | if (fwrite(&b1, sizeof(uint8_t), 1, file) < 1) return -1; 303 | } 304 | else { 305 | uint8_t b0 = value & 0b01111111; 306 | if (fwrite(&b0, sizeof(uint8_t), 1, file) < 1) return -1; 307 | } 308 | return 0; 309 | } 310 | 311 | Size: 312 | 313 | size_t Uint7Size(uint32_t value) { 314 | if (value >= 2097152) { 315 | return 4; 316 | } 317 | else if (value >= 16384) { 318 | return 3; 319 | } 320 | else if (value >= 128) { 321 | return 2; 322 | } 323 | else { 324 | return 1; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /opblib.c: -------------------------------------------------------------------------------- 1 | /* 2 | // MIT License 3 | // 4 | // Copyright (c) 2021 Eniko Fox/Emma Maassen 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | */ 24 | #ifdef _WIN32 25 | #define _CRT_SECURE_NO_DEPRECATE 26 | #endif 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include "opblib.h" 33 | 34 | #define OPB_HEADER_SIZE 7 35 | // OPBin1\0 36 | const char OPB_Header[OPB_HEADER_SIZE] = { 'O', 'P', 'B', 'i', 'n', '1', '\0' }; 37 | 38 | #define VECTOR_MIN_CAPACITY 8 39 | #define VECTOR_PTR(vector, index) (void*)((uint8_t*)((vector)->Storage) + (index) * vector->ElementSize) 40 | // this only exists to make type declarations clearer 41 | #define VectorT(T) Vector 42 | 43 | typedef struct Vector { 44 | size_t Count; 45 | size_t Capacity; 46 | size_t ElementSize; 47 | void* Storage; 48 | } Vector; 49 | 50 | Vector Vector_New(size_t elementSize) { 51 | Vector v = { 0 }; 52 | v.ElementSize = elementSize; 53 | return v; 54 | } 55 | 56 | static void Vector_Free(Vector* v) { 57 | if (v->Storage != NULL) { 58 | free(v->Storage); 59 | } 60 | v->Storage = NULL; 61 | v->Capacity = 0; 62 | v->Count = 0; 63 | } 64 | 65 | static void* Vector_Get(Vector* v, int index) { 66 | if (index < 0 || index >= v->Count) { 67 | return NULL; 68 | } 69 | if (v->ElementSize <= 0) { 70 | return NULL; 71 | } 72 | return VECTOR_PTR(v, index); 73 | } 74 | #define Vector_GetT(T, vector, index) ((T*)Vector_Get(vector, index)) 75 | 76 | static int Vector_Set(Vector* v, void* item, int index) { 77 | if (index < 0 || index >= v->Count) { 78 | return -1; 79 | } 80 | if (v->ElementSize <= 0) { 81 | return -1; 82 | } 83 | memcpy(VECTOR_PTR(v, index), item, v->ElementSize); 84 | return 0; 85 | } 86 | 87 | static int Vector_Add(Vector* v, void* item) { 88 | if (v->ElementSize <= 0) { 89 | return -1; 90 | } 91 | if (v->Count >= v->Capacity) { 92 | size_t newCapacity = v->Capacity * 2; 93 | if (newCapacity < VECTOR_MIN_CAPACITY) newCapacity = VECTOR_MIN_CAPACITY; 94 | 95 | void* newStorage = malloc(newCapacity * v->ElementSize); 96 | if (newStorage == NULL) { 97 | return -1; 98 | } 99 | 100 | if (v->Storage != NULL) { 101 | memcpy(newStorage, v->Storage, v->Count * v->ElementSize); 102 | free(v->Storage); 103 | } 104 | 105 | v->Storage = newStorage; 106 | v->Capacity = newCapacity; 107 | } 108 | 109 | v->Count++; 110 | return Vector_Set(v, item, (size_t)((int)v->Count - 1)); 111 | } 112 | 113 | static int Vector_AddRange(Vector* v, void* items, size_t count) { 114 | if (v->ElementSize <= 0) { 115 | return -1; 116 | } 117 | uint8_t* itemBytes = (uint8_t*)items; 118 | for (size_t i = 0; i < count; i++, itemBytes += v->ElementSize) { 119 | int ret; 120 | ret = Vector_Add(v, (void*)itemBytes); 121 | if (ret) return ret; 122 | } 123 | return 0; 124 | } 125 | 126 | typedef int(*VectorSortFunc)(const void* a, const void* b); 127 | 128 | static void Vector_Clear(Vector* v, bool keepStorage) { 129 | v->Count = 0; 130 | if (!keepStorage && v->Storage != NULL) { 131 | free(v->Storage); 132 | v->Storage = NULL; 133 | v->Capacity = 0; 134 | } 135 | } 136 | 137 | static void Vector_Sort(Vector* v, VectorSortFunc sortFunc) { 138 | qsort(v->Storage, v->Count, v->ElementSize, sortFunc); 139 | } 140 | 141 | static const char* GetFilename(const char* path) { 142 | const char* lastFwd = strrchr(path, '/'); 143 | const char* lastBck = strrchr(path, '\\'); 144 | if (lastFwd == NULL && lastBck == NULL) { 145 | return path; 146 | } 147 | return lastFwd > lastBck ? lastFwd + 1 : lastBck + 1; 148 | } 149 | 150 | static const char* GetSourceFilename(void) { 151 | return GetFilename(__FILE__); 152 | } 153 | 154 | #define CONCAT_IMPL(x, y) x##y 155 | #define MACRO_CONCAT(x, y) CONCAT_IMPL(x, y) 156 | 157 | #define NUM_CHANNELS 18 158 | #define NUM_TRACKS (NUM_CHANNELS + 1) 159 | 160 | #define WRITE(buffer, size, count, context) \ 161 | if (context->Write(buffer, size, count, context->UserData) != count) { \ 162 | Log("OPB write error occurred in '%s' at line %d\n", GetSourceFilename(), __LINE__); \ 163 | return OPBERR_WRITE_ERROR; \ 164 | } 165 | 166 | #define WRITE_UINT7(context, value) \ 167 | if (WriteUint7(context, value)) { \ 168 | Log("OPB write error occurred in '%s' at line %d\n", GetSourceFilename(), __LINE__); \ 169 | return OPBERR_WRITE_ERROR; \ 170 | } 171 | 172 | #define SEEK(context, offset, origin) \ 173 | if (context->Seek(context->UserData, offset, origin)) { \ 174 | Log("OPB seek error occurred in '%s' at line %d\n", GetSourceFilename(), __LINE__); \ 175 | return OPBERR_SEEK_ERROR; \ 176 | } 177 | #define TELL(context, var) \ 178 | var = context->Tell(context->UserData); \ 179 | if (var == -1L) { \ 180 | Log("OPB file position error occurred in '%s' at line %d\n", GetSourceFilename(), __LINE__); \ 181 | return OPBERR_TELL_ERROR; \ 182 | } 183 | 184 | #define READ(buffer, size, count, context) \ 185 | if (context->Read(buffer, size, count, context->UserData) != count) { \ 186 | Log("OPB read error occurred in '%s' at line %d\n", GetSourceFilename(), __LINE__); \ 187 | return OPBERR_READ_ERROR; \ 188 | } 189 | #define READ_UINT7(var, context) \ 190 | if ((var = ReadUint7(context)) < 0) { \ 191 | Log("OPB read error occurred in '%s' at line %d\n", GetSourceFilename(), __LINE__); \ 192 | return OPBERR_READ_ERROR; \ 193 | } 194 | 195 | #define SUBMIT(stream, count, context) \ 196 | if (context->Submit(stream, count, context->ReceiverData)) return OPBERR_BUFFER_ERROR 197 | 198 | typedef struct Context Context; 199 | typedef struct Command Command; 200 | typedef struct OpbData OpbData; 201 | typedef struct Instrument Instrument; 202 | 203 | typedef struct Context { 204 | VectorT(Command) CommandStream; 205 | OPB_StreamWriter Write; 206 | OPB_StreamSeeker Seek; 207 | OPB_StreamTeller Tell; 208 | OPB_StreamReader Read; 209 | OPB_BufferReceiver Submit; 210 | OPB_Format Format; 211 | VectorT(OpbData) DataMap; 212 | VectorT(Instrument) Instruments; 213 | VectorT(Command) Tracks[NUM_TRACKS]; 214 | double Time; 215 | void* UserData; 216 | void* ReceiverData; 217 | } Context; 218 | 219 | static void Context_Free(Context* context) { 220 | if (context->CommandStream.Storage != NULL) { Vector_Free(&context->CommandStream); } 221 | if (context->Instruments.Storage != NULL) { Vector_Free(&context->Instruments); } 222 | if (context->DataMap.Storage != NULL) { Vector_Free(&context->DataMap); } 223 | for (int i = 0; i < NUM_TRACKS; i++) { 224 | if (context->Tracks[i].Storage != NULL) { Vector_Free(&context->Tracks[i]); } 225 | } 226 | } 227 | 228 | OPB_LogHandler OPB_Log; 229 | 230 | static inline size_t BufferSize(const char* format, ...) { 231 | va_list args; 232 | va_start(args, format); 233 | size_t result = vsnprintf(NULL, 0, format, args); 234 | va_end(args); 235 | return (size_t)(result + 1); // safe byte for \0 236 | } 237 | 238 | static void Log(const char* format, ...) { 239 | if (!OPB_Log) return; 240 | 241 | va_list args; 242 | 243 | va_start(args, format); 244 | size_t size = BufferSize(format, args); 245 | va_end(args); 246 | 247 | if (size == 0) return; 248 | 249 | va_start(args, format); 250 | char* s = NULL; 251 | if (size < 0 || (s = (char*)malloc(size)) == NULL) { 252 | vprintf(format, args); 253 | } 254 | else { 255 | vsprintf(s, format, args); 256 | } 257 | va_end(args); 258 | 259 | if (s != NULL) { 260 | OPB_Log(s); 261 | free(s); 262 | } 263 | } 264 | 265 | const char* OPB_GetFormatName(OPB_Format fmt) { 266 | switch (fmt) { 267 | default: 268 | return "Default"; 269 | case OPB_Format_Raw: 270 | return "Raw"; 271 | } 272 | } 273 | 274 | static inline uint32_t FlipEndian32(uint32_t val) { 275 | #ifdef OPB_BIG_ENDIAN 276 | return val; 277 | #else 278 | val = ((val << 8) & 0xFF00FF00) | ((val >> 8) & 0xFF00FF); 279 | return (val << 16) | (val >> 16); 280 | #endif 281 | } 282 | 283 | static inline uint16_t FlipEndian16(uint16_t val) { 284 | #ifdef OPB_BIG_ENDIAN 285 | return val; 286 | #else 287 | return (val << 8) | ((val >> 8) & 0xFF); 288 | #endif 289 | } 290 | 291 | static size_t Uint7Size(uint32_t value) { 292 | if (value >= 2097152) { 293 | return 4; 294 | } 295 | else if (value >= 16384) { 296 | return 3; 297 | } 298 | else if (value >= 128) { 299 | return 2; 300 | } 301 | else { 302 | return 1; 303 | } 304 | } 305 | 306 | typedef struct Command { 307 | uint16_t Addr; 308 | uint8_t Data; 309 | double Time; 310 | int OrderIndex; 311 | int DataIndex; 312 | } Command; 313 | 314 | typedef struct OpbData { 315 | uint32_t Count; 316 | uint8_t Args[16]; 317 | } OpbData; 318 | 319 | static void OpbData_WriteUint7(OpbData* data, uint32_t value) { 320 | if (value >= 2097152) { 321 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 322 | uint8_t b1 = ((value & 0b011111110000000) >> 7) | 0b10000000; 323 | uint8_t b2 = ((value & 0b0111111100000000000000) >> 14) | 0b10000000; 324 | uint8_t b3 = (value & 0b11111111000000000000000000000) >> 21; 325 | data->Args[data->Count] = b0; data->Count++; 326 | data->Args[data->Count] = b1; data->Count++; 327 | data->Args[data->Count] = b2; data->Count++; 328 | data->Args[data->Count] = b3; data->Count++; 329 | } 330 | else if (value >= 16384) { 331 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 332 | uint8_t b1 = ((value & 0b011111110000000) >> 7) | 0b10000000; 333 | uint8_t b2 = (value & 0b0111111100000000000000) >> 14; 334 | data->Args[data->Count] = b0; data->Count++; 335 | data->Args[data->Count] = b1; data->Count++; 336 | data->Args[data->Count] = b2; data->Count++; 337 | } 338 | else if (value >= 128) { 339 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 340 | uint8_t b1 = (value & 0b011111110000000) >> 7; 341 | data->Args[data->Count] = b0; data->Count++; 342 | data->Args[data->Count] = b1; data->Count++; 343 | } 344 | else { 345 | uint8_t b0 = value & 0b01111111; 346 | data->Args[data->Count] = b0; data->Count++; 347 | } 348 | } 349 | 350 | static void OpbData_WriteU8(OpbData* data, uint32_t value) { 351 | data->Args[data->Count] = (uint8_t)value; 352 | data->Count++; 353 | } 354 | 355 | #define OPB_CMD_SETINSTRUMENT 0xD0 356 | #define OPB_CMD_PLAYINSTRUMENT 0xD1 357 | #define OPB_CMD_NOTEON 0xD7 358 | 359 | static inline bool IsSpecialCommand(int addr) { 360 | addr = addr & 0xFF; 361 | return addr >= 0xD0 && addr <= 0xDF; 362 | } 363 | 364 | #define NUM_OPERATORS 36 365 | static int OperatorOffsets[NUM_OPERATORS] = { 366 | 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 367 | 0x100, 0x101, 0x102, 0x103, 0x104, 0x105, 0x108, 0x109, 0x10A, 0x10B, 0x10C, 0x10D, 0x110, 0x111, 0x112, 0x113, 0x114, 0x115, 368 | }; 369 | 370 | static int ChannelToOp[NUM_CHANNELS] = { 371 | 0, 1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20, 24, 25, 26, 30, 31, 32, 372 | }; 373 | 374 | static int ChannelToOffset[NUM_CHANNELS] = { 375 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 376 | 0x100, 0x101, 0x102, 0x103, 0x104, 0x105, 0x106, 0x107, 0x108, 377 | }; 378 | 379 | static int RegisterOffsetToChannel(uint32_t offset) { 380 | uint32_t baseoff = offset & 0xFF; 381 | int chunk = baseoff / 8; 382 | int suboff = baseoff % 8; 383 | 384 | if (chunk >= 3 || suboff >= 6) { 385 | return -1; 386 | } 387 | return chunk * 3 + (suboff % 3) + ((offset & 0x100) != 0 ? NUM_CHANNELS / 2 : 0); 388 | } 389 | 390 | static int RegisterOffsetToOpIndex(uint32_t offset) { 391 | uint32_t baseoff = offset & 0xFF; 392 | uint32_t suboff = baseoff % 8; 393 | if (suboff >= 6) { 394 | return -1; 395 | } 396 | return suboff >= 3; 397 | } 398 | 399 | #define REG_FEEDCONN 0xC0 400 | #define REG_CHARACTER 0x20 401 | #define REG_LEVELS 0x40 402 | #define REG_ATTACK 0x60 403 | #define REG_SUSTAIN 0x80 404 | #define REG_WAVE 0xE0 405 | #define REG_FREQUENCY 0xA0 406 | #define REG_NOTE 0xB0 407 | 408 | typedef struct Operator { 409 | int16_t Characteristic; 410 | int16_t AttackDecay; 411 | int16_t SustainRelease; 412 | int16_t WaveSelect; 413 | } Operator; 414 | 415 | typedef struct Instrument { 416 | int16_t FeedConn; 417 | Operator Modulator; 418 | Operator Carrier; 419 | int Index; 420 | } Instrument; 421 | 422 | static Context Context_New(void) { 423 | Context context = { 0 }; 424 | 425 | context.CommandStream = Vector_New(sizeof(Command)); 426 | context.Instruments = Vector_New(sizeof(Instrument)); 427 | context.DataMap = Vector_New(sizeof(OpbData)); 428 | for (int i = 0; i < NUM_TRACKS; i++) { 429 | context.Tracks[i] = Vector_New(sizeof(Command)); 430 | } 431 | 432 | return context; 433 | } 434 | 435 | static bool CanCombineInstrument(Instrument* instr, Command* feedconn, 436 | Command* modChar, Command* modAttack, Command* modSustain, Command* modWave, 437 | Command* carChar, Command* carAttack, Command* carSustain, Command* carWave) { 438 | if ((feedconn == NULL || instr->FeedConn == feedconn->Data || instr->FeedConn < 0) && 439 | (modChar == NULL || instr->Modulator.Characteristic == modChar->Data || instr->Modulator.Characteristic < 0) && 440 | (modAttack == NULL || instr->Modulator.AttackDecay == modAttack->Data || instr->Modulator.AttackDecay < 0) && 441 | (modSustain == NULL || instr->Modulator.SustainRelease == modSustain->Data || instr->Modulator.SustainRelease < 0) && 442 | (modWave == NULL || instr->Modulator.WaveSelect == modWave->Data || instr->Modulator.WaveSelect < 0) && 443 | (carChar == NULL || instr->Carrier.Characteristic == carChar->Data || instr->Carrier.Characteristic < 0) && 444 | (carAttack == NULL || instr->Carrier.AttackDecay == carAttack->Data || instr->Carrier.AttackDecay < 0) && 445 | (carSustain == NULL || instr->Carrier.SustainRelease == carSustain->Data || instr->Carrier.SustainRelease < 0) && 446 | (carWave == NULL || instr->Carrier.WaveSelect == carWave->Data) || instr->Carrier.WaveSelect < 0) { 447 | instr->FeedConn = feedconn != NULL ? feedconn->Data : instr->FeedConn; 448 | instr->Modulator.Characteristic = modChar != NULL ? modChar->Data : instr->Modulator.Characteristic; 449 | instr->Modulator.AttackDecay = modAttack != NULL ? modAttack->Data : instr->Modulator.AttackDecay; 450 | instr->Modulator.SustainRelease = modSustain != NULL ? modSustain->Data : instr->Modulator.SustainRelease; 451 | instr->Modulator.WaveSelect = modWave != NULL ? modWave->Data : instr->Modulator.WaveSelect; 452 | instr->Carrier.Characteristic = carChar != NULL ? carChar->Data : instr->Carrier.Characteristic; 453 | instr->Carrier.AttackDecay = carAttack != NULL ? carAttack->Data : instr->Carrier.AttackDecay; 454 | instr->Carrier.SustainRelease = carSustain != NULL ? carSustain->Data : instr->Carrier.SustainRelease; 455 | instr->Carrier.WaveSelect = carWave != NULL ? carWave->Data : instr->Carrier.WaveSelect; 456 | return true; 457 | } 458 | return false; 459 | } 460 | 461 | static Instrument GetInstrument(Context* context, Command* feedconn, 462 | Command* modChar, Command* modAttack, Command* modSustain, Command* modWave, 463 | Command* carChar, Command* carAttack, Command* carSustain, Command* carWave) { 464 | // find a matching instrument 465 | for (int i = 0; i < context->Instruments.Count; i++) { 466 | Instrument* instr = Vector_GetT(Instrument, &context->Instruments, i); 467 | if (CanCombineInstrument(instr, feedconn, modChar, modAttack, modSustain, modWave, carChar, carAttack, carSustain, carWave)) { 468 | return *instr; 469 | } 470 | } 471 | 472 | // no instrument found, create and store new instrument 473 | Instrument instr = { 474 | feedconn == NULL ? -1 : feedconn->Data, 475 | { 476 | modChar == NULL ? -1 : modChar->Data, 477 | modAttack == NULL ? -1 : modAttack->Data, 478 | modSustain == NULL ? -1 : modSustain->Data, 479 | modWave == NULL ? -1 : modWave->Data, 480 | }, 481 | { 482 | carChar == NULL ? -1 : carChar->Data, 483 | carAttack == NULL ? -1 : carAttack->Data, 484 | carSustain == NULL ? -1 : carSustain->Data, 485 | carWave == NULL ? -1 : carWave->Data, 486 | }, 487 | (int)context->Instruments.Count 488 | }; 489 | Vector_Add(&context->Instruments, &instr); 490 | return instr; 491 | } 492 | 493 | static int WriteInstrument(Context* context, const Instrument* instr) { 494 | uint8_t feedConn = (uint8_t)(instr->FeedConn >= 0 ? instr->FeedConn : 0); 495 | uint8_t modChr = (uint8_t)(instr->Modulator.Characteristic >= 0 ? instr->Modulator.Characteristic : 0); 496 | uint8_t modAtk = (uint8_t)(instr->Modulator.AttackDecay >= 0 ? instr->Modulator.AttackDecay : 0); 497 | uint8_t modSus = (uint8_t)(instr->Modulator.SustainRelease >= 0 ? instr->Modulator.SustainRelease : 0); 498 | uint8_t modWav = (uint8_t)(instr->Modulator.WaveSelect >= 0 ? instr->Modulator.WaveSelect : 0); 499 | uint8_t carChr = (uint8_t)(instr->Carrier.Characteristic >= 0 ? instr->Carrier.Characteristic : 0); 500 | uint8_t carAtk = (uint8_t)(instr->Carrier.AttackDecay >= 0 ? instr->Carrier.AttackDecay : 0); 501 | uint8_t carSus = (uint8_t)(instr->Carrier.SustainRelease >= 0 ? instr->Carrier.SustainRelease : 0); 502 | uint8_t carWav = (uint8_t)(instr->Carrier.WaveSelect >= 0 ? instr->Carrier.WaveSelect : 0); 503 | 504 | WRITE(&feedConn, sizeof(uint8_t), 1, context); 505 | WRITE(&modChr, sizeof(uint8_t), 1, context); 506 | WRITE(&modAtk, sizeof(uint8_t), 1, context); 507 | WRITE(&modSus, sizeof(uint8_t), 1, context); 508 | WRITE(&modWav, sizeof(uint8_t), 1, context); 509 | WRITE(&carChr, sizeof(uint8_t), 1, context); 510 | WRITE(&carAtk, sizeof(uint8_t), 1, context); 511 | WRITE(&carSus, sizeof(uint8_t), 1, context); 512 | WRITE(&carWav, sizeof(uint8_t), 1, context); 513 | 514 | return 0; 515 | } 516 | 517 | static int WriteUint7(Context* context, uint32_t value) { 518 | if (value >= 2097152) { 519 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 520 | uint8_t b1 = ((value & 0b011111110000000) >> 7) | 0b10000000; 521 | uint8_t b2 = ((value & 0b0111111100000000000000) >> 14) | 0b10000000; 522 | uint8_t b3 = (value & 0b11111111000000000000000000000) >> 21; 523 | if (context->Write(&b0, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 524 | if (context->Write(&b1, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 525 | if (context->Write(&b2, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 526 | if (context->Write(&b3, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 527 | } 528 | else if (value >= 16384) { 529 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 530 | uint8_t b1 = ((value & 0b011111110000000) >> 7) | 0b10000000; 531 | uint8_t b2 = (value & 0b0111111100000000000000) >> 14; 532 | if (context->Write(&b0, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 533 | if (context->Write(&b1, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 534 | if (context->Write(&b2, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 535 | } 536 | else if (value >= 128) { 537 | uint8_t b0 = (value & 0b01111111) | 0b10000000; 538 | uint8_t b1 = (value & 0b011111110000000) >> 7; 539 | if (context->Write(&b0, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 540 | if (context->Write(&b1, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 541 | } 542 | else { 543 | uint8_t b0 = value & 0b01111111; 544 | if (context->Write(&b0, sizeof(uint8_t), 1, context->UserData) < 1) return -1; 545 | } 546 | return 0; 547 | } 548 | 549 | // returns channel for note event or -1 if not a note event 550 | static int IsNoteEvent(int addr) { 551 | int baseAddr = addr & 0xFF; 552 | if (baseAddr >= 0xB0 && baseAddr <= 0xB8) { 553 | return (baseAddr - 0xB0) * ((addr & 0x100) == 0 ? 1 : 2); 554 | } 555 | else if (baseAddr >= OPB_CMD_NOTEON && baseAddr < OPB_CMD_NOTEON + NUM_CHANNELS / 2) { 556 | return (baseAddr - OPB_CMD_NOTEON) * ((addr & 0x100) == 0 ? 1 : 2); 557 | } 558 | return -1; 559 | } 560 | 561 | static bool IsChannelNoteEvent(int addr, int channel) { 562 | return 563 | (addr == 0xB0 + (channel % 9) + (channel >= 9 ? 0x100 : 0)) || 564 | (addr == OPB_CMD_NOTEON + (channel % 9) + (channel >= 9 ? 0x100 : 0)); 565 | } 566 | 567 | static int ChannelFromRegister(int reg) { 568 | int baseReg = reg & 0xFF; 569 | if ((baseReg >= 0x20 && baseReg <= 0x95) || (baseReg >= 0xE0 && baseReg <= 0xF5)) { 570 | int offset = baseReg % 0x20; 571 | if (offset < 0 || offset >= 0x16) { 572 | return -1; 573 | } 574 | if ((reg & 0x100) != 0) { 575 | offset |= 0x100; 576 | } 577 | int ch; 578 | if ((ch = RegisterOffsetToChannel(offset)) >= 0) { 579 | return ch; 580 | } 581 | } 582 | else if ((baseReg >= 0xA0 && baseReg <= 0xB8) || (baseReg >= 0xC0 && baseReg <= 0xC8)) { 583 | int ch = baseReg % 0x10; 584 | if (ch < 0 || ch >= 0x9) { 585 | return -1; 586 | } 587 | if ((reg & 0x100) != 0) { 588 | ch += 9; 589 | } 590 | return ch; 591 | } 592 | return -1; 593 | } 594 | 595 | // 0 for modulator, 1 for carrier, -1 otherwise 596 | static int RegisterToOpIndex(int reg) { 597 | int baseReg = reg & 0xFF; 598 | if ((baseReg >= 0x20 && baseReg <= 0x95) || (baseReg >= 0xE0 && baseReg <= 0xF5)) { 599 | int offset = baseReg % 0x20; 600 | if (offset < 0 || offset >= 0x16) { 601 | return -1; 602 | } 603 | int op; 604 | if ((op = RegisterOffsetToOpIndex(offset)) >= 0) { 605 | return op; 606 | } 607 | } 608 | return -1; 609 | } 610 | 611 | static void SeparateTracks(Context* context) { 612 | for (int i = 0; i < context->CommandStream.Count; i++) { 613 | Command* cmd = Vector_GetT(Command, &context->CommandStream, i); 614 | 615 | int channel = ChannelFromRegister(cmd->Addr); 616 | if (channel < 0) channel = NUM_TRACKS - 1; 617 | 618 | Vector_Add(&context->Tracks[channel], cmd); 619 | } 620 | } 621 | 622 | static int CountInstrumentChanges(Command* feedconn, 623 | Command* modChar, Command* modAttack, Command* modSustain, Command* modWave, 624 | Command* carChar, Command* carAttack, Command* carSustain, Command* carWave) { 625 | int count = 0; 626 | if (feedconn != NULL) count++; 627 | if (modChar != NULL) count++; 628 | if (modAttack != NULL) count++; 629 | if (modSustain != NULL) count++; 630 | if (modWave != NULL) count++; 631 | if (carChar != NULL) count++; 632 | if (carAttack != NULL) count++; 633 | if (carSustain != NULL) count++; 634 | if (carWave != NULL) count++; 635 | return count; 636 | } 637 | 638 | static int ProcessRange(Context* context, int channel, double time, Command* commands, int cmdCount, Vector* range, 639 | int _debug_start, int _debug_end // these last two are only for logging in case of error 640 | ) { 641 | for (int i = 0; i < cmdCount; i++) { 642 | Command* cmd = commands + i; 643 | 644 | if (cmd->Time != time) { 645 | int timeMs = (int)(time * 1000); 646 | Log("A timing error occurred at %d ms on channel %d in range %d-%d\n", timeMs, channel, _debug_start, _debug_end); 647 | return OPBERR_LOGGED; 648 | } 649 | } 650 | 651 | Command* modChar = NULL, * modLevel = NULL, * modAttack = NULL, * modSustain = NULL, * modWave = NULL; 652 | Command* carChar = NULL, * carLevel = NULL, * carAttack = NULL, * carSustain = NULL, * carWave = NULL; 653 | Command* freq = NULL, * note = NULL, * feedconn = NULL; 654 | 655 | for (int i = 0; i < cmdCount; i++) { 656 | Command* cmd = commands + i; 657 | 658 | int baseAddr = cmd->Addr & 0xFF; 659 | int op; 660 | 661 | if ((op = RegisterToOpIndex(cmd->Addr)) > -1) { 662 | // command affects modulator or carrier 663 | if (op == 0) { 664 | if (baseAddr >= 0x20 && baseAddr <= 0x35) 665 | modChar = cmd; 666 | else if (baseAddr >= 0x40 && baseAddr <= 0x55) 667 | modLevel = cmd; 668 | else if (baseAddr >= 0x60 && baseAddr <= 0x75) 669 | modAttack = cmd; 670 | else if (baseAddr >= 0x80 && baseAddr <= 0x95) 671 | modSustain = cmd; 672 | else if (baseAddr >= 0xE0 && baseAddr <= 0xF5) 673 | modWave = cmd; 674 | } 675 | else { 676 | if (baseAddr >= 0x20 && baseAddr <= 0x35) 677 | carChar = cmd; 678 | else if (baseAddr >= 0x40 && baseAddr <= 0x55) 679 | carLevel = cmd; 680 | else if (baseAddr >= 0x60 && baseAddr <= 0x75) 681 | carAttack = cmd; 682 | else if (baseAddr >= 0x80 && baseAddr <= 0x95) 683 | carSustain = cmd; 684 | else if (baseAddr >= 0xE0 && baseAddr <= 0xF5) 685 | carWave = cmd; 686 | } 687 | } 688 | else { 689 | if (baseAddr >= 0xA0 && baseAddr <= 0xA8) 690 | freq = cmd; 691 | else if (baseAddr >= 0xB0 && baseAddr <= 0xB8) { 692 | if (note != NULL) { 693 | int timeMs = (int)(time * 1000); 694 | Log("A decoding error occurred at %d ms on channel %d in range %d-%d\n", timeMs, channel, _debug_start, _debug_end); 695 | return OPBERR_LOGGED; 696 | } 697 | note = cmd; 698 | } 699 | else if (baseAddr >= 0xC0 && baseAddr <= 0xC8) 700 | feedconn = cmd; 701 | else { 702 | Vector_Add(range, cmd); 703 | } 704 | } 705 | } 706 | 707 | // combine instrument data 708 | int instrChanges; 709 | if ((instrChanges = CountInstrumentChanges(feedconn, modChar, modAttack, modSustain, modWave, carChar, carAttack, carSustain, carWave)) > 0) { 710 | Instrument instr = GetInstrument(context, feedconn, modChar, modAttack, modSustain, modWave, carChar, carAttack, carSustain, carWave); 711 | 712 | size_t size = Uint7Size(instr.Index) + 3; 713 | 714 | if (modLevel != NULL) { 715 | size++; 716 | instrChanges++; 717 | } 718 | if (carLevel != NULL) { 719 | size++; 720 | instrChanges++; 721 | } 722 | 723 | // combine with frequency and note command if present 724 | if (freq != NULL && note != NULL) { 725 | size += 2; 726 | instrChanges += 2; 727 | } 728 | 729 | if ((int)size < instrChanges * 2) { 730 | OpbData data = { 0 }; 731 | OpbData_WriteUint7(&data, instr.Index); 732 | 733 | uint8_t channelMask = channel | 734 | (modLevel != NULL ? 0b00100000 : 0) | 735 | (carLevel != NULL ? 0b01000000 : 0) | 736 | (feedconn != NULL ? 0b10000000 : 0); 737 | OpbData_WriteU8(&data, channelMask); 738 | 739 | int mask = 740 | (modChar != NULL ? 0b00000001 : 0) | 741 | (modAttack != NULL ? 0b00000010 : 0) | 742 | (modSustain != NULL ? 0b00000100 : 0) | 743 | (modWave != NULL ? 0b00001000 : 0) | 744 | (carChar != NULL ? 0b00010000 : 0) | 745 | (carAttack != NULL ? 0b00100000 : 0) | 746 | (carSustain != NULL ? 0b01000000 : 0) | 747 | (carWave != NULL ? 0b10000000 : 0); 748 | OpbData_WriteU8(&data, mask); 749 | 750 | // instrument command is 0xD0 751 | int reg = OPB_CMD_SETINSTRUMENT; 752 | 753 | if (freq != NULL && note != NULL) { 754 | OpbData_WriteU8(&data, freq->Data); 755 | OpbData_WriteU8(&data, note->Data); 756 | 757 | // play command is 0xD1 758 | reg = OPB_CMD_PLAYINSTRUMENT; 759 | } 760 | 761 | if (modLevel != NULL) OpbData_WriteU8(&data, modLevel->Data); 762 | if (carLevel != NULL) OpbData_WriteU8(&data, carLevel->Data); 763 | 764 | int opbIndex = (int32_t)context->DataMap.Count + 1; 765 | Vector_Add(&context->DataMap, &data); 766 | 767 | Command cmd = { 768 | (uint16_t)(reg + (channel >= 9 ? 0x100 : 0)), // register 769 | 0, // data 770 | time, 771 | commands[0].OrderIndex, 772 | opbIndex 773 | }; 774 | 775 | Vector_Add(range, &cmd); 776 | feedconn = modChar = modLevel = modAttack = modSustain = modWave = carChar = carLevel = carAttack = carSustain = carWave = NULL; 777 | 778 | if (freq != NULL && note != NULL) { 779 | freq = note = NULL; 780 | } 781 | } 782 | } 783 | 784 | // combine frequency/note and modulator and carrier level data 785 | if (freq != NULL && note != NULL) { 786 | // note on command is 0xD7 through 0xDF (and 0x1D7 through 0x1DF for channels 10-18) 787 | int reg = OPB_CMD_NOTEON + (channel % 9) + (channel >= 9 ? 0x100 : 0); 788 | 789 | OpbData data = { 0 }; 790 | OpbData_WriteU8(&data, freq->Data); 791 | 792 | int noteLevels = note->Data & 0b00111111; 793 | 794 | // encode modulator and carrier levels data in the note data's upper 2 (unused) bits 795 | if (modLevel != NULL) { 796 | noteLevels |= 0b01000000; 797 | } 798 | if (carLevel != NULL) { 799 | noteLevels |= 0b10000000; 800 | } 801 | 802 | OpbData_WriteU8(&data, noteLevels); 803 | 804 | if (modLevel != NULL) { 805 | OpbData_WriteU8(&data, modLevel->Data); 806 | } 807 | if (carLevel != NULL) { 808 | OpbData_WriteU8(&data, carLevel->Data); 809 | } 810 | 811 | int opbIndex = (int32_t)context->DataMap.Count + 1; 812 | Vector_Add(&context->DataMap, &data); 813 | 814 | Command cmd = { 815 | (uint16_t)reg, // register 816 | 0, // data 817 | time, 818 | note->OrderIndex, 819 | opbIndex 820 | }; 821 | 822 | Vector_Add(range, &cmd); 823 | freq = note = modLevel = carLevel = NULL; 824 | } 825 | 826 | if (modChar != NULL) Vector_Add(range, modChar); 827 | if (modLevel != NULL) Vector_Add(range, modLevel); 828 | if (modAttack != NULL) Vector_Add(range, modAttack); 829 | if (modSustain != NULL) Vector_Add(range, modSustain); 830 | if (modWave != NULL) Vector_Add(range, modWave); 831 | 832 | if (carChar != NULL) Vector_Add(range, carChar); 833 | if (carLevel != NULL) Vector_Add(range, carLevel); 834 | if (carAttack != NULL) Vector_Add(range, carAttack); 835 | if (carSustain != NULL) Vector_Add(range, carSustain); 836 | if (carWave != NULL) Vector_Add(range, carWave); 837 | 838 | if (feedconn != NULL) Vector_Add(range, feedconn); 839 | if (freq != NULL) Vector_Add(range, freq); 840 | if (note != NULL) Vector_Add(range, note); 841 | 842 | return 0; 843 | } 844 | 845 | static int ProcessTrack(Context* context, int channel, Vector* chOut) { 846 | Vector* commands = &context->Tracks[channel]; 847 | 848 | if (commands->Count == 0) { 849 | return 0; 850 | } 851 | 852 | int lastOrder = Vector_GetT(Command, commands, 0)->OrderIndex; 853 | int i = 0; 854 | 855 | while (i < commands->Count) { 856 | double time = Vector_GetT(Command, commands, i)->Time; 857 | 858 | int start = i; 859 | // sequences must be all in the same time block and in order 860 | // sequences are capped by a note command (write to register B0-B8 or 1B0-1B8) 861 | while (i < commands->Count && Vector_GetT(Command, commands, i)->Time <= time && (Vector_GetT(Command, commands, i)->OrderIndex - lastOrder) <= 1) { 862 | Command* cmd = Vector_GetT(Command, commands, i); 863 | 864 | lastOrder = cmd->OrderIndex; 865 | i++; 866 | 867 | if (IsChannelNoteEvent(cmd->Addr, channel)) { 868 | break; 869 | } 870 | } 871 | int end = i; 872 | 873 | VectorT(Command) range = Vector_New(sizeof(Command)); 874 | int ret = ProcessRange(context, channel, time, Vector_GetT(Command, commands, start), end - start, &range, start, end); 875 | if (ret) { 876 | Vector_Free(&range); 877 | return ret; 878 | } 879 | 880 | Vector_AddRange(chOut, range.Storage, range.Count); 881 | Vector_Free(&range); 882 | 883 | if (i < commands->Count) { 884 | lastOrder = Vector_GetT(Command, commands, i)->OrderIndex; 885 | } 886 | } 887 | 888 | return 0; 889 | } 890 | 891 | static int WriteChunk(Context* context, double elapsed, int start, int count) { 892 | uint32_t elapsedMs = (uint32_t)((elapsed * 1000) + 0.5); 893 | int loCount = 0; 894 | int hiCount = 0; 895 | 896 | for (int i = start; i < start + count; i++) { 897 | Command* cmd = Vector_GetT(Command, &context->CommandStream, i); 898 | 899 | if ((cmd->Addr & 0x100) == 0) { 900 | loCount++; 901 | } 902 | else { 903 | hiCount++; 904 | } 905 | } 906 | 907 | // write header 908 | WRITE_UINT7(context, elapsedMs); 909 | WRITE_UINT7(context, loCount); 910 | WRITE_UINT7(context, hiCount); 911 | 912 | // write low and high register writes 913 | bool isLow = true; 914 | while (true) { 915 | for (int i = start; i < start + count; i++) { 916 | Command* cmd = Vector_GetT(Command, &context->CommandStream, i); 917 | 918 | if (((cmd->Addr & 0x100) == 0) == isLow) { 919 | uint8_t baseAddr = cmd->Addr & 0xFF; 920 | WRITE(&baseAddr, sizeof(uint8_t), 1, context); 921 | 922 | if (cmd->DataIndex) { 923 | if (!IsSpecialCommand(baseAddr)) { 924 | Log("Unexpected write error. Command had DataIndex but was not an OPB command\n"); 925 | return OPBERR_LOGGED; 926 | } 927 | 928 | // opb command 929 | OpbData* data = Vector_GetT(OpbData, &context->DataMap, cmd->DataIndex - 1); 930 | WRITE(data->Args, sizeof(uint8_t), data->Count, context); 931 | } 932 | else { 933 | if (IsSpecialCommand(baseAddr)) { 934 | Log("Unexpected write error. Command was an OPB command but had no DataIndex\n"); 935 | return OPBERR_LOGGED; 936 | } 937 | 938 | // regular write 939 | WRITE(&(cmd->Data), sizeof(uint8_t), 1, context); 940 | } 941 | } 942 | } 943 | 944 | if (!isLow) { 945 | break; 946 | } 947 | 948 | isLow = !isLow; 949 | } 950 | 951 | return 0; 952 | } 953 | 954 | static int SortCommands(const void* a, const void* b) { 955 | return ((Command*)a)->OrderIndex - ((Command*)b)->OrderIndex; 956 | } 957 | 958 | static int ConvertToOpb(Context* context) { 959 | if (context->Format < OPB_Format_Default || context->Format > OPB_Format_Raw) { 960 | context->Format = OPB_Format_Default; 961 | } 962 | 963 | WRITE(OPB_Header, sizeof(char), OPB_HEADER_SIZE, context); 964 | 965 | Log("OPB format %d (%s)\n", context->Format, OPB_GetFormatName(context->Format)); 966 | 967 | uint8_t fmt = (uint8_t)context->Format; 968 | WRITE(&fmt, sizeof(uint8_t), 1, context); 969 | 970 | if (context->Format == OPB_Format_Raw) { 971 | Log("Writing raw OPL data stream\n"); 972 | 973 | double lastTime = 0.0; 974 | for (int i = 0; i < context->CommandStream.Count; i++) { 975 | Command* cmd = Vector_GetT(Command, &context->CommandStream, i); 976 | 977 | uint16_t elapsed = FlipEndian16((uint16_t)((cmd->Time - lastTime) * 1000.0)); 978 | uint16_t addr = FlipEndian16(cmd->Addr); 979 | 980 | WRITE(&elapsed, sizeof(uint16_t), 1, context); 981 | WRITE(&addr, sizeof(uint16_t), 1, context); 982 | WRITE(&(cmd->Data), sizeof(uint8_t), 1, context); 983 | lastTime = cmd->Time; 984 | } 985 | return 0; 986 | } 987 | 988 | // separate command stream into tracks 989 | Log("Separating OPL data stream into channels\n"); 990 | SeparateTracks(context); 991 | 992 | // process each track into its own output vector 993 | VectorT(Command) chOut[NUM_TRACKS]; 994 | 995 | for (int i = 0; i < NUM_TRACKS; i++) { 996 | Log("Processing channel %d\n", i); 997 | chOut[i] = Vector_New(sizeof(Command)); 998 | 999 | int ret = ProcessTrack(context, i, chOut + i); 1000 | if (ret) { 1001 | for (int j = 0; j < NUM_TRACKS; j++) { 1002 | Vector_Free(chOut + j); 1003 | } 1004 | return ret; 1005 | } 1006 | } 1007 | 1008 | // combine all output back into command stream 1009 | Log("Combining processed data into linear stream\n"); 1010 | Vector_Clear(&context->CommandStream, true); 1011 | for (int i = 0; i < NUM_TRACKS; i++) { 1012 | Vector_AddRange(&context->CommandStream, chOut[i].Storage, chOut[i].Count); 1013 | } 1014 | 1015 | for (int j = 0; j < NUM_TRACKS; j++) { 1016 | Vector_Free(chOut + j); 1017 | } 1018 | 1019 | // sort by received order 1020 | Vector_Sort(&context->CommandStream, SortCommands); 1021 | 1022 | // write instruments table 1023 | SEEK(context, 12, SEEK_CUR); // skip header 1024 | 1025 | Log("Writing instrument table\n"); 1026 | for (int i = 0; i < context->Instruments.Count; i++) { 1027 | int ret = WriteInstrument(context, Vector_GetT(Instrument, &context->Instruments, i)); 1028 | if (ret) return ret; 1029 | } 1030 | 1031 | // write chunks 1032 | { 1033 | int chunks = 0; 1034 | double lastTime = 0; 1035 | int i = 0; 1036 | 1037 | Log("Writing chunks\n"); 1038 | while (i < context->CommandStream.Count) { 1039 | double chunkTime = Vector_GetT(Command, &context->CommandStream, i)->Time; 1040 | 1041 | int start = i; 1042 | while (i < context->CommandStream.Count && Vector_GetT(Command, &context->CommandStream, i)->Time <= chunkTime) { 1043 | i++; 1044 | } 1045 | int end = i; 1046 | 1047 | int ret = WriteChunk(context, chunkTime - lastTime, start, end - start); 1048 | if (ret) return ret; 1049 | chunks++; 1050 | 1051 | lastTime = chunkTime; 1052 | } 1053 | 1054 | // write header 1055 | Log("Writing header\n"); 1056 | 1057 | long fpos; 1058 | TELL(context, fpos); 1059 | 1060 | uint32_t length = FlipEndian32(fpos); 1061 | uint32_t instrCount = FlipEndian32((uint32_t)context->Instruments.Count); 1062 | uint32_t chunkCount = FlipEndian32(chunks); 1063 | 1064 | SEEK(context, OPB_HEADER_SIZE + 1, SEEK_SET); 1065 | WRITE(&length, sizeof(uint32_t), 1, context); 1066 | WRITE(&instrCount, sizeof(uint32_t), 1, context); 1067 | WRITE(&chunkCount, sizeof(uint32_t), 1, context); 1068 | } 1069 | 1070 | return 0; 1071 | } 1072 | 1073 | static size_t WriteToFile(const void* buffer, size_t elementSize, size_t elementCount, void* context) { 1074 | return fwrite(buffer, elementSize, elementCount, (FILE*)context); 1075 | } 1076 | 1077 | static int SeekInFile(void* context, long offset, int origin) { 1078 | return fseek((FILE*)context, offset, origin); 1079 | } 1080 | 1081 | static long TellInFile(void* context) { 1082 | return ftell((FILE*)context); 1083 | } 1084 | 1085 | int OPB_OplToFile(OPB_Format format, OPB_Command* commandStream, size_t commandCount, const char* file) { 1086 | FILE* outFile; 1087 | if ((outFile = fopen(file, "wb")) == NULL) { 1088 | Log("Couldn't open file '%s' for writing\n", file); 1089 | return OPBERR_LOGGED; 1090 | } 1091 | int ret = OPB_OplToBinary(format, commandStream, commandCount, WriteToFile, SeekInFile, TellInFile, outFile); 1092 | if (fclose(outFile)) { 1093 | Log("Error while closing file '%s'\n", file); 1094 | return OPBERR_LOGGED; 1095 | } 1096 | return ret; 1097 | } 1098 | 1099 | int OPB_OplToBinary(OPB_Format format, OPB_Command* commandStream, size_t commandCount, OPB_StreamWriter write, OPB_StreamSeeker seek, OPB_StreamTeller tell, void* userData) { 1100 | Context context = Context_New(); 1101 | 1102 | context.Write = write; 1103 | context.Seek = seek; 1104 | context.Tell = tell; 1105 | context.UserData = userData; 1106 | context.Format = format; 1107 | 1108 | // convert stream to internal format 1109 | int orderIndex = 0; 1110 | for (int i = 0; i < commandCount; i++) { 1111 | const OPB_Command* source = commandStream + i; 1112 | 1113 | if (IsSpecialCommand(source->Addr)) { 1114 | Log("Illegal register 0x%03X with value 0x%02X in command stream, ignored\n", source->Addr, source->Data); 1115 | } 1116 | else { 1117 | Command cmd = { 1118 | source->Addr, // OPL register 1119 | source->Data, // OPL data 1120 | source->Time, // Time in seconds 1121 | orderIndex++, // Stream index 1122 | 0 // Data index 1123 | }; 1124 | 1125 | Vector_Add(&context.CommandStream, &cmd); 1126 | } 1127 | } 1128 | 1129 | int ret = ConvertToOpb(&context); 1130 | Context_Free(&context); 1131 | 1132 | if (ret) { 1133 | Log("%s\n", OPB_GetErrorMessage(ret)); 1134 | } 1135 | 1136 | return ret; 1137 | } 1138 | 1139 | static int ReadInstrument(Context* context, Instrument* instr) { 1140 | uint8_t buffer[9]; 1141 | READ(buffer, sizeof(uint8_t), 9, context); 1142 | *instr = (Instrument) { 1143 | buffer[0], // feedconn 1144 | { 1145 | buffer[1], // modulator characteristic 1146 | buffer[2], // modulator attack/decay 1147 | buffer[3], // modulator sustain/release 1148 | buffer[4], // modulator wave select 1149 | }, 1150 | { 1151 | buffer[5], // carrier characteristic 1152 | buffer[6], // carrier attack/decay 1153 | buffer[7], // carrier sustain/release 1154 | buffer[8], // carrier wave select 1155 | }, 1156 | (int)context->Instruments.Count // instrument index 1157 | }; 1158 | return 0; 1159 | } 1160 | 1161 | static int ReadUint7(Context* context) { 1162 | uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; 1163 | 1164 | if (context->Read(&b0, sizeof(uint8_t), 1, context->UserData) != 1) return -1; 1165 | if (b0 >= 128) { 1166 | b0 &= 0b01111111; 1167 | if (context->Read(&b1, sizeof(uint8_t), 1, context->UserData) != 1) return -1; 1168 | if (b1 >= 128) { 1169 | b1 &= 0b01111111; 1170 | if (context->Read(&b2, sizeof(uint8_t), 1, context->UserData) != 1) return -1; 1171 | if (b2 >= 128) { 1172 | b2 &= 0b01111111; 1173 | if (context->Read(&b3, sizeof(uint8_t), 1, context->UserData) != 1) return -1; 1174 | } 1175 | } 1176 | } 1177 | 1178 | return b0 | (b1 << 7) | (b2 << 14) | (b3 << 21); 1179 | } 1180 | 1181 | #define DEFAULT_READBUFFER_SIZE 256 1182 | 1183 | static inline int AddToBuffer(Context* context, OPB_Command* buffer, int* index, OPB_Command cmd) { 1184 | buffer[*index] = cmd; 1185 | (*index)++; 1186 | 1187 | if (*index >= DEFAULT_READBUFFER_SIZE) { 1188 | SUBMIT(buffer, DEFAULT_READBUFFER_SIZE, context); 1189 | *index = 0; 1190 | } 1191 | 1192 | return 0; 1193 | } 1194 | 1195 | #define ADD_TO_BUFFER_IMPL(retvar, context, buffer, index, ...) \ 1196 | { int retvar; \ 1197 | if ((retvar = AddToBuffer(context, buffer, bufferIndex, (OPB_Command) __VA_ARGS__))) return retvar; } 1198 | #define ADD_TO_BUFFER(context, buffer, index, ...) ADD_TO_BUFFER_IMPL(MACRO_CONCAT(__ret, __LINE__), context, buffer, index, __VA_ARGS__) 1199 | 1200 | static int ReadCommand(Context* context, OPB_Command* buffer, int* bufferIndex, int mask) { 1201 | uint8_t baseAddr; 1202 | READ(&baseAddr, sizeof(uint8_t), 1, context); 1203 | 1204 | int addr = baseAddr | mask; 1205 | 1206 | switch (baseAddr) { 1207 | default: { 1208 | uint8_t data; 1209 | READ(&data, sizeof(uint8_t), 1, context); 1210 | ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)addr, data, context->Time }); 1211 | break; 1212 | } 1213 | 1214 | case OPB_CMD_PLAYINSTRUMENT: 1215 | case OPB_CMD_SETINSTRUMENT: { 1216 | int instrIndex; 1217 | READ_UINT7(instrIndex, context); 1218 | 1219 | uint8_t channelMask[2]; 1220 | READ(channelMask, sizeof(uint8_t), 2, context); 1221 | 1222 | int channel = channelMask[0]; 1223 | bool modLvl = (channel & 0b00100000) != 0; 1224 | bool carLvl = (channel & 0b01000000) != 0; 1225 | bool feedconn = (channel & 0b10000000) != 0; 1226 | channel &= 0b00011111; 1227 | 1228 | if (channel < 0 || channel >= NUM_CHANNELS) { 1229 | Log("Error reading OPB command: channel %d out of range\n", channel); 1230 | return OPBERR_LOGGED; 1231 | } 1232 | 1233 | int chmask = channelMask[1]; 1234 | bool modChr = (chmask & 0b00000001) != 0; 1235 | bool modAtk = (chmask & 0b00000010) != 0; 1236 | bool modSus = (chmask & 0b00000100) != 0; 1237 | bool modWav = (chmask & 0b00001000) != 0; 1238 | bool carChr = (chmask & 0b00010000) != 0; 1239 | bool carAtk = (chmask & 0b00100000) != 0; 1240 | bool carSus = (chmask & 0b01000000) != 0; 1241 | bool carWav = (chmask & 0b10000000) != 0; 1242 | 1243 | uint8_t freq = 0, note = 0; 1244 | bool isPlay = baseAddr == OPB_CMD_PLAYINSTRUMENT; 1245 | if (isPlay) { 1246 | READ(&freq, sizeof(uint8_t), 1, context); 1247 | READ(¬e, sizeof(uint8_t), 1, context); 1248 | } 1249 | 1250 | uint8_t modLvlData = 0, carLvlData = 0; 1251 | if (modLvl) READ(&modLvlData, sizeof(uint8_t), 1, context); 1252 | if (carLvl) READ(&carLvlData, sizeof(uint8_t), 1, context); 1253 | 1254 | if (instrIndex < 0 || instrIndex >= context->Instruments.Count) { 1255 | Log("Error reading OPB command: instrument %d out of range\n", instrIndex); 1256 | return OPBERR_LOGGED; 1257 | } 1258 | 1259 | Instrument* instr = Vector_GetT(Instrument, &context->Instruments, instrIndex); 1260 | int conn = ChannelToOffset[channel]; 1261 | int mod = OperatorOffsets[ChannelToOp[channel]]; 1262 | int car = mod + 3; 1263 | int playOffset = ChannelToOffset[channel]; 1264 | 1265 | if (feedconn) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_FEEDCONN + conn), (uint8_t)instr->FeedConn, context->Time }); 1266 | if (modChr) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_CHARACTER + mod), (uint8_t)instr->Modulator.Characteristic, context->Time }); 1267 | if (modLvl) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_LEVELS + mod), modLvlData, context->Time }); 1268 | if (modAtk) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_ATTACK + mod), (uint8_t)instr->Modulator.AttackDecay, context->Time }); 1269 | if (modSus) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_SUSTAIN + mod), (uint8_t)instr->Modulator.SustainRelease, context->Time }); 1270 | if (modWav) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_WAVE + mod), (uint8_t)instr->Modulator.WaveSelect, context->Time }); 1271 | if (carChr) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_CHARACTER + car), (uint8_t)instr->Carrier.Characteristic, context->Time }); 1272 | if (carLvl) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_LEVELS + car), carLvlData, context->Time }); 1273 | if (carAtk) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_ATTACK + car), (uint8_t)instr->Carrier.AttackDecay, context->Time }); 1274 | if (carSus) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_SUSTAIN + car), (uint8_t)instr->Carrier.SustainRelease, context->Time }); 1275 | if (carWav) ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_WAVE + car), (uint8_t)instr->Carrier.WaveSelect, context->Time }); 1276 | if (isPlay) { 1277 | ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_FREQUENCY + playOffset), freq, context->Time }); 1278 | ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(REG_NOTE + playOffset), note, context->Time }); 1279 | } 1280 | 1281 | break; 1282 | } 1283 | 1284 | case OPB_CMD_NOTEON: 1285 | case OPB_CMD_NOTEON + 1: 1286 | case OPB_CMD_NOTEON + 2: 1287 | case OPB_CMD_NOTEON + 3: 1288 | case OPB_CMD_NOTEON + 4: 1289 | case OPB_CMD_NOTEON + 5: 1290 | case OPB_CMD_NOTEON + 6: 1291 | case OPB_CMD_NOTEON + 7: 1292 | case OPB_CMD_NOTEON + 8: { 1293 | int channel = (baseAddr - 0xD7) + (mask != 0 ? 9 : 0); 1294 | 1295 | if (channel < 0 || channel >= NUM_CHANNELS) { 1296 | Log("Error reading OPB command: channel %d out of range\n", channel); 1297 | return OPBERR_LOGGED; 1298 | } 1299 | 1300 | uint8_t freqNote[2]; 1301 | READ(freqNote, sizeof(uint8_t), 2, context); 1302 | 1303 | uint8_t freq = freqNote[0]; 1304 | uint8_t note = freqNote[1]; 1305 | 1306 | ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(addr - (OPB_CMD_NOTEON - REG_FREQUENCY)), freq, context->Time }); 1307 | ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)(addr - (OPB_CMD_NOTEON - REG_NOTE)), (uint8_t)(note & 0b00111111), context->Time }); 1308 | 1309 | if ((note & 0b01000000) != 0) { 1310 | // set modulator volume 1311 | uint8_t vol; 1312 | READ(&vol, sizeof(uint8_t), 1, context); 1313 | int reg = REG_LEVELS + OperatorOffsets[ChannelToOp[channel]]; 1314 | ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)reg, vol, context->Time }); 1315 | } 1316 | if ((note & 0b10000000) != 0) { 1317 | // set carrier volume 1318 | uint8_t vol; 1319 | READ(&vol, sizeof(uint8_t), 1, context); 1320 | int reg = REG_LEVELS + 3 + OperatorOffsets[ChannelToOp[channel]]; 1321 | ADD_TO_BUFFER(context, buffer, bufferIndex, { (uint16_t)reg, vol, context->Time }); 1322 | } 1323 | break; 1324 | } 1325 | } 1326 | 1327 | return 0; 1328 | } 1329 | 1330 | static int ReadChunk(Context* context, OPB_Command* buffer, int* bufferIndex) { 1331 | int elapsed, loCount, hiCount; 1332 | 1333 | READ_UINT7(elapsed, context); 1334 | READ_UINT7(loCount, context); 1335 | READ_UINT7(hiCount, context); 1336 | 1337 | context->Time += elapsed / 1000.0; 1338 | 1339 | for (int i = 0; i < loCount; i++) { 1340 | int ret = ReadCommand(context, buffer, bufferIndex, 0x0); 1341 | if (ret) return ret; 1342 | } 1343 | for (int i = 0; i < hiCount; i++) { 1344 | int ret = ReadCommand(context, buffer, bufferIndex, 0x100); 1345 | if (ret) return ret; 1346 | } 1347 | 1348 | return 0; 1349 | } 1350 | 1351 | static int ReadOpbDefault(Context* context) { 1352 | uint32_t header[3]; 1353 | READ(header, sizeof(uint32_t), 3, context); 1354 | for (int i = 0; i < 3; i++) header[i] = FlipEndian32(header[i]); 1355 | 1356 | uint32_t instrumentCount = header[1]; 1357 | uint32_t chunkCount = header[2]; 1358 | 1359 | for (uint32_t i = 0; i < instrumentCount; i++) { 1360 | Instrument instr; 1361 | int ret = ReadInstrument(context, &instr); 1362 | if (ret) return ret; 1363 | Vector_Add(&context->Instruments, &instr); 1364 | } 1365 | 1366 | OPB_Command buffer[DEFAULT_READBUFFER_SIZE]; 1367 | int bufferIndex = 0; 1368 | 1369 | for (uint32_t i = 0; i < chunkCount; i++) { 1370 | int ret = ReadChunk(context, buffer, &bufferIndex); 1371 | if (ret) return ret; 1372 | } 1373 | 1374 | if (bufferIndex > 0) { 1375 | SUBMIT(buffer, bufferIndex, context); 1376 | } 1377 | 1378 | return 0; 1379 | } 1380 | 1381 | #define RAW_READBUFFER_SIZE 256 1382 | #define RAW_ENTRY_SIZE 5 1383 | 1384 | static int ReadOpbRaw(Context* context) { 1385 | double time = 0; 1386 | uint8_t buffer[RAW_READBUFFER_SIZE * RAW_ENTRY_SIZE]; 1387 | OPB_Command commandStream[RAW_READBUFFER_SIZE]; 1388 | 1389 | size_t itemsRead; 1390 | while ((itemsRead = context->Read(buffer, RAW_ENTRY_SIZE, RAW_READBUFFER_SIZE, context->UserData)) > 0) { 1391 | uint8_t* value = buffer; 1392 | 1393 | for (int i = 0; i < itemsRead; i++, value += RAW_ENTRY_SIZE) { 1394 | uint16_t elapsed = (value[0] << 8) | value[1]; 1395 | uint16_t addr = (value[2] << 8) | value[3]; 1396 | uint8_t data = value[4]; 1397 | 1398 | time += elapsed / 1000.0; 1399 | 1400 | OPB_Command cmd = { 1401 | addr, 1402 | data, 1403 | time 1404 | }; 1405 | commandStream[i] = cmd; 1406 | } 1407 | SUBMIT(commandStream, itemsRead, context); 1408 | } 1409 | 1410 | return 0; 1411 | } 1412 | 1413 | static int ConvertFromOpb(Context* context) { 1414 | char id[OPB_HEADER_SIZE + 1] = { 0 }; 1415 | READ(id, sizeof(char), OPB_HEADER_SIZE, context); 1416 | 1417 | if (strncmp(id, "OPBin", 5)) { 1418 | return OPBERR_NOT_AN_OPB_FILE; 1419 | } 1420 | 1421 | switch (id[5]) { 1422 | case '1': 1423 | break; 1424 | default: 1425 | return OPBERR_VERSION_UNSUPPORTED; 1426 | } 1427 | 1428 | if (id[6] != '\0') { 1429 | return OPBERR_NOT_AN_OPB_FILE; 1430 | } 1431 | 1432 | uint8_t fmt; 1433 | READ(&fmt, sizeof(uint8_t), 1, context); 1434 | 1435 | switch (fmt) { 1436 | default: 1437 | Log("Error reading OPB file: unknown format %d\n", fmt); 1438 | return OPBERR_LOGGED; 1439 | case OPB_Format_Default: 1440 | return ReadOpbDefault(context); 1441 | case OPB_Format_Raw: 1442 | return ReadOpbRaw(context); 1443 | } 1444 | } 1445 | 1446 | static size_t ReadFromFile(void* buffer, size_t elementSize, size_t elementCount, void* context) { 1447 | return fread(buffer, elementSize, elementCount, (FILE*)context); 1448 | } 1449 | 1450 | int OPB_FileToOpl(const char* file, OPB_BufferReceiver receiver, void* receiverData) { 1451 | FILE* inFile; 1452 | if ((inFile = fopen(file, "rb")) == NULL) { 1453 | Log("Couldn't open file '%s' for reading\n", file); 1454 | return OPBERR_LOGGED; 1455 | } 1456 | int ret = OPB_BinaryToOpl(ReadFromFile, inFile, receiver, receiverData); 1457 | fclose(inFile); 1458 | return ret; 1459 | } 1460 | 1461 | int OPB_BinaryToOpl(OPB_StreamReader reader, void* readerData, OPB_BufferReceiver receiver, void* receiverData) { 1462 | Context context = { 0 }; 1463 | 1464 | context.Read = reader; 1465 | context.Submit = receiver; 1466 | context.UserData = readerData; 1467 | context.ReceiverData = receiverData; 1468 | context.Instruments = Vector_New(sizeof(Instrument)); 1469 | 1470 | int ret = ConvertFromOpb(&context); 1471 | Context_Free(&context); 1472 | 1473 | if (ret) { 1474 | Log("%s\n", OPB_GetErrorMessage(ret)); 1475 | } 1476 | 1477 | return ret; 1478 | } 1479 | 1480 | const char* OPB_GetErrorMessage(int errCode) { 1481 | switch (errCode) { 1482 | case OPBERR_WRITE_ERROR: 1483 | return "A write error occurred while converting OPB"; 1484 | break; 1485 | case OPBERR_SEEK_ERROR: 1486 | return "A seek error occurred while converting OPB"; 1487 | break; 1488 | case OPBERR_TELL_ERROR: 1489 | return "A file position error occurred while converting OPB"; 1490 | break; 1491 | case OPBERR_READ_ERROR: 1492 | return "A read error occurred while converting OPB"; 1493 | break; 1494 | case OPBERR_BUFFER_ERROR: 1495 | return "A buffer error occurred while converting OPB"; 1496 | break; 1497 | case OPBERR_NOT_AN_OPB_FILE: 1498 | return "Couldn't parse OPB file; not a valid OPB file"; 1499 | break; 1500 | case OPBERR_VERSION_UNSUPPORTED: 1501 | return "Couldn't parse OPB file; invalid version or version unsupported"; 1502 | break; 1503 | default: 1504 | return "Unknown OPB error"; 1505 | } 1506 | } -------------------------------------------------------------------------------- /opblib.h: -------------------------------------------------------------------------------- 1 | /* 2 | // MIT License 3 | // 4 | // Copyright (c) 2021 Eniko Fox/Emma Maassen 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | */ 24 | #pragma once 25 | #include 26 | 27 | #ifdef __cplusplus 28 | extern "C" { 29 | #endif 30 | 31 | // uncomment this for big endian architecture 32 | //#define OPB_BIG_ENDIAN 33 | 34 | #define OPBERR_LOGGED 1 // an error occurred and what error that was has been sent to OPB_Log 35 | #define OPBERR_WRITE_ERROR 2 36 | #define OPBERR_SEEK_ERROR 3 37 | #define OPBERR_TELL_ERROR 4 38 | #define OPBERR_READ_ERROR 5 39 | #define OPBERR_BUFFER_ERROR 6 40 | #define OPBERR_NOT_AN_OPB_FILE 7 41 | #define OPBERR_VERSION_UNSUPPORTED 8 42 | 43 | typedef struct OPB_Command { 44 | uint16_t Addr; 45 | uint8_t Data; 46 | double Time; 47 | } OPB_Command; 48 | 49 | typedef enum OPB_Format { 50 | OPB_Format_Default, 51 | OPB_Format_Raw, 52 | } OPB_Format; 53 | 54 | const char* OPB_GetErrorMessage(int errCode); 55 | 56 | const char* OPB_GetFormatName(OPB_Format fmt); 57 | 58 | // Custom write handler of the same form as stdio.h's fwrite for writing to memory 59 | // This function should write elementSize * elementCount bytes from buffer to the user-defined context object 60 | // Must return elementCount if successful 61 | typedef size_t(*OPB_StreamWriter)(const void* buffer, size_t elementSize, size_t elementCount, void* context); 62 | 63 | // Custom seek handler of the same form as stdio.h's fseek for writing to memory 64 | // This function should change the position to write to in the user-defined context object by the number of bytes 65 | // Specified by offset, relative to the specified origin which is one of 3 values: 66 | // 67 | // 1. Beginning of file (same as fseek's SEEK_SET) 68 | // 2. Current position of the file pointer (same as fseek's SEEK_CUR) 69 | // 3. End of file (same as fseek's SEEK_END) 70 | // 71 | // Must return 0 if successful 72 | typedef int (*OPB_StreamSeeker)(void* context, long offset, int origin); 73 | 74 | // Custom tell handler of the same form as stdio.h's ftell for writing to memory 75 | // This function must return the current write position for the user-defined context object 76 | // Must return -1L if unsuccessful 77 | typedef long (*OPB_StreamTeller)(void* context); 78 | 79 | // Custom read handler of the same form as stdio.h's fread for reading from memory 80 | // This function should read elementSize * elementCount bytes from the user-defined context object to buffer 81 | // Should return number of elements read 82 | typedef size_t(*OPB_StreamReader)(void* buffer, size_t elementSize, size_t elementCount, void* context); 83 | 84 | // Function that receives OPB_Command items read by OPB_BinaryToOpl and OPB_FileToOpl 85 | // This is where you copy the OPB_Command items into a data structure or the user-defined context object 86 | // Should return 0 if successful. Note that the array for `commandStream` is stack allocated and must be copied! 87 | typedef int(*OPB_BufferReceiver)(OPB_Command* commandStream, size_t commandCount, void* context); 88 | 89 | // OPL command stream to binary. Returns 0 if successful. 90 | int OPB_OplToBinary(OPB_Format format, OPB_Command* commandStream, size_t commandCount, 91 | OPB_StreamWriter write, OPB_StreamSeeker seek, OPB_StreamTeller tell, void* userData); 92 | 93 | // OPL command stream to file. Returns 0 if successful. 94 | int OPB_OplToFile(OPB_Format format, OPB_Command* commandStream, size_t commandCount, const char* file); 95 | 96 | // OPB binary to OPL command stream. Returns 0 if successful. 97 | int OPB_BinaryToOpl(OPB_StreamReader reader, void* readerData, OPB_BufferReceiver receiver, void* receiverData); 98 | 99 | // OPB file to OPL command stream. Returns 0 if successful. 100 | int OPB_FileToOpl(const char* file, OPB_BufferReceiver receiver, void* receiverData); 101 | 102 | // OPBLib log function 103 | typedef void (*OPB_LogHandler)(const char* s); 104 | extern OPB_LogHandler OPB_Log; 105 | 106 | #ifdef __cplusplus 107 | } 108 | #endif 109 | --------------------------------------------------------------------------------