├── .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 |
--------------------------------------------------------------------------------