1
0
mirror of https://github.com/ScrelliCopter/VGM-Tools synced 2025-02-21 04:09:25 +11:00

61 Commits

Author SHA1 Message Date
effcf727ac normalise macros 2023-12-29 06:21:32 +11:00
78790991b6 add .editorconfig 2023-12-29 06:15:28 +11:00
47f6b23943 neotools: neoadpcmextract now converts ADPCM-B to Wave 2023-12-29 04:04:28 +11:00
5b28c18472 neotools: make adpcmb decoder work on nibble pairs like adpcma 2023-12-29 03:39:33 +11:00
70bbf3d0d1 neotools: make adpcmb encoder & decoder streamable (encoder untested) 2023-12-29 03:25:14 +11:00
9f6c0664ff neotools: use common wave writer in adpcm.c 2023-12-29 02:27:11 +11:00
4013d1809c neotools: factor adpcmb.c encode/decode into functions 2023-12-29 01:35:14 +11:00
802bdef961 neotools: implement adpcma extraction in neoadpcmextract 2023-12-29 00:53:59 +11:00
ae14868953 neotools: adpcm-a split backend code 2023-12-10 11:36:05 +11:00
46c78c24e1 neotools: adpcm-a partial rewrite 2023-12-10 11:28:52 +11:00
ff41b5415e neotools: adpcm.c replace errorlog with stderr 2023-12-10 10:48:57 +11:00
7764375ec9 neotools: delete old shell scripts 2023-12-10 10:42:23 +11:00
e334ad82cc neotools: remove autoextract.c and move main logic into the main file 2023-12-10 10:41:22 +11:00
353d4e5def neotools: code fixup & vgz support 2023-12-10 10:37:15 +11:00
9c5e19264b wavetable stuff 2023-11-07 02:43:28 +11:00
111f800c49 generalise python wave writer 2023-11-07 01:34:29 +11:00
dbce8e5c29 normalise macro formatting 2023-03-21 14:28:36 +11:00
c1f36bd322 oops lol 2023-03-21 14:26:00 +11:00
9946144995 combine all (except spctools) as subprojects 2023-03-21 14:25:14 +11:00
05008f5c47 get adpcm tools working on mac and somewhat unify the codebase 2023-03-20 11:52:50 +11:00
f70bcd0a50 Fix apple build and sleep deprivation code 2023-03-20 10:09:28 +11:00
7c9c2464cb dsptool: basic dspdecode implementation 2023-03-19 21:32:18 +11:00
99bf061438 dsptool: refactor 2023-03-19 14:16:06 +11:00
ac89564669 DspTool dump 2023-03-19 08:45:10 +11:00
9cdae38cbf update licenses 2023-03-19 08:30:02 +11:00
5a059441df Prob not a bad idea to link back here in feropm.py 2020-12-15 21:51:27 +11:00
a8cd2f0f36 Create README.md 2020-10-31 15:32:54 +11:00
695f6a1bf1 Fix oversight with the source path ending up the patch name & use output name when specified 2020-10-09 11:48:12 +11:00
862639b4fe Add crummy OPN/OPM preset conversion tool shat out in a sleep deprived stupor 2020-10-09 11:28:59 +11:00
5cfb861369 Refactor most of the opening and scanning between sources 2020-07-08 21:28:04 +10:00
6456404bd3 neotools: fuck it, we CMake now 2020-05-26 10:29:57 +10:00
08b61568e1 add stub for loading vgz directly 2019-10-03 00:44:27 +10:00
a76bb43ec1 makefile builds objects into a build artifacts directory
also forgot to commit the header
2019-10-02 12:37:08 +10:00
2a654f25e8 move main to a new source file, update makefile to support multiple sources 2019-10-02 12:26:12 +10:00
47df3e2177 add some basic error checking in DecodeSample 2019-10-02 10:59:07 +10:00
6510096f90 malloc cast is unnecessary in C 2019-10-01 23:50:01 +10:00
07ee0b546c rename and build as C 2019-10-01 23:46:52 +10:00
454fcd3226 use c include for stdint 2019-10-01 23:40:22 +10:00
c94016e793 use a lightweight buffer class for in-memory samples 2019-10-01 23:37:55 +10:00
172174c837 import stdio instead of string 2019-10-01 23:26:19 +10:00
3fafdfd3f4 use c string for out names 2019-10-01 23:25:13 +10:00
6ccf9f6e38 snprintf for output names 2019-10-01 23:23:38 +10:00
b12258970e replace cout with printf 2019-09-30 23:17:47 +10:00
886966d7dc merge DecodeSample & DumpBytes 2019-09-30 20:43:07 +10:00
63aacb9a01 FILE io for DumpBytes 2019-09-30 20:33:58 +10:00
7c66bbab52 fix some error prone stuff, also I goofed the short circuit... oops 2019-09-30 17:14:07 +10:00
fbf887b534 c io for input 2019-09-29 11:51:14 +10:00
0a26bc8998 reformat 2019-09-28 10:18:39 +10:00
6d60ea91c0 update README.md 2019-09-27 16:16:50 +10:00
6164533c1a port autoextract to shell 2019-09-27 15:58:08 +10:00
7c5a577fba makefile for crappy vgm pcm scanner util 2019-09-26 22:54:08 +10:00
7e0740d03a adpcm decoders compiling on loonix 2019-09-26 22:06:51 +10:00
c1a5003a82 batch should be crlf I guess 2019-09-26 14:53:40 +10:00
1969f7ae9a add spc2it manpage from upstream 2019-09-26 14:43:11 +10:00
33851696b1 move relevant gitignore 2019-09-26 14:24:37 +10:00
7aff052810 fix spelling mistake 2019-09-26 14:22:28 +10:00
85a3e5089b update spc2it README 2019-09-26 14:19:39 +10:00
508f464aa7 housekeeping 2019-09-26 14:06:37 +10:00
86317f57ec create plain makefile for spc2it 2019-09-26 14:01:18 +10:00
d0945220a6 Cleaned up some loose semicolons... old habits die hard. 2018-10-10 23:14:38 +11:00
45ec9cf2cf Reorganised slightly, and added my spc sample ripper + spc2it. 2018-09-03 02:45:47 +10:00
63 changed files with 9088 additions and 862 deletions

22
.editorconfig Normal file
View File

@@ -0,0 +1,22 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
tab_width = 4
[{CMakeLists.txt,*.cmake}]
indent_style = tab
indent_size = tab
max_line_length = 120
[*.{c,cc,cpp,h,hpp,hh,m,mm}]
indent_style = tab
indent_size = tab
max_line_length = 120
[*.py]
indent_style = tab
indent_size = tab
max_line_length = 120

19
.gitignore vendored
View File

@@ -1,6 +1,19 @@
.idea/
**/build/
**/cmake-build-*/
*.exe *.exe
*.vgm
*.vgz
*.log *.log
*.pcm
*.wav *.wav
neotools/**/*.pcm
neotools/**/*.vgm
neotools/**/*.vgz
spctools/it
spctools/spc
spctools/sample
FM Harmonics/
.DS_Store

10
CMakeLists.txt Normal file
View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
project(VGM-Tools LANGUAGES C)
set(WARNINGS
$<$<C_COMPILER_ID:AppleClang,Clang,GNU>:-Wall -Wextra -pedantic -Wno-unused-parameter>
$<$<C_COMPILER_ID:MSVC>:/Wall /wd4100>)
add_subdirectory(common)
add_subdirectory(dsptools)
add_subdirectory(neotools)

15
COPYING.zlib Normal file
View File

@@ -0,0 +1,15 @@
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

View File

@@ -1,32 +1,11 @@
Neo-Geo VGM tools # [VGM-Tools](https://github.com/ScrelliCopter/VGM-Tools)
-----------------
A hodge-podge of tools for working on NG VGM files, Personal scratch pad for various VGM extraction/transformation projects.
these files are provided in the hope that they may be useful to someone.
Makefiles have currently not been tested but shouldn't be hard to get working. Tools that are used in scripts are usually included in-tree and may be out of date or augmented from their upstream versions.
Windows binaries are available under the Releases tab.
Included tools (sources included). All original code is Zlib licensed (see COPYING.zlib for details), other copyrights will be specified where appropriate.
- **adpcm.exe**:
ADPCM Type-A to WAV converter.
- **adpcmb.exe**:
ADPCM Type-B encoding tool that also does decoding to WAV.
- **neoadpcmextract.exe**:
Scans a .vgm and dumps all ADPCM type A&B data to raw .pcm files.
- **autoextract.bat**:
Convenience batch script that uses the above tools to dump all samples to WAVs.
How to use This repository is in a constant state of flux and things may not work or compile at any one time, you may want to look at the [Releases](https://github.com/ScrelliCopter/VGM-Tools/releases) tab for snapshots of the repository that may prove more useful.
----------
Copy your .vgz into this directory, drag it onto autoextract.bat,
the script will create a directory of the same name and dump all the converted samples there.
That's all there is to it. The tools herein are primarily made for my own use or some kind of one-off problem, but they are provided here in the hopes they may be useful to someone.
TODO
----
- Replace the batch script with something less shite (and actually cross-platform).
- Write a makefile for neoadpcmextract.
- Make this stuff build on Linux.

View File

@@ -1,70 +0,0 @@
# Standard makefile to use as a base for DJGPP projects
# By MARTINEZ Fabrice aka SNK of SUPREMACY
# Programs to use during make
AR = ar
CC = gcc
LD = gcc
ASM = nasm
PACKER = upx
# Flags for debugging
#DEBUG=1
#SYMBOLS=1
# Flags for compilation
ASMFLAGS = -f coff
ifdef SYMBOLS
CFLAGS = -o -mpentium -Wall -Werror -g
LDFLAGS = -s
else
CFLAGS = -fomit-frame-pointer -O3 -mpentium -Werror -Wall \
-W -Wno-sign-compare -Wno-unused \
-Wpointer-arith -Wbad-function-cast -Wcast-align -Waggregate-return \
-pedantic \
-Wshadow \
-Wstrict-prototypes
LDFLAGS =
endif
CDEFS =
ASMDEFS =
# Object files
MAINOBJS =
# Library files
MAINLIBS = obj/adpcm.a
# Make rules
all: adpcm.exe
adpcm.exe: $(MAINOBJS) $(MAINLIBS)
$(LD) $(LDFLAGS) $(MAINOBJS) $(MAINLIBS) -o $@
src/%.asm:
obj/%.o: src/%.c
$(CC) $(CDEFS) $(CFLAGS) -c $< -o $@
obj/%.oa: src/%.asm
$(ASM) -o $@ $(ASMFLAGS) $(ASMDEFS) $<
obj/%.a:
$(AR) cr $@ $^
# Rules to manage files
pack: adpcm.exe
$(PACKER) adpcm.exe
mkdir:
md obj
clean:
del obj\*.o
del obj\*.a
del obj\*.oa
del *.exe
# Rules to make libraries
obj/adpcm.a: obj/adpcm.o
# obj/decode.oa \

158
adpcm.c
View File

@@ -1,158 +0,0 @@
#include <process.h>
#include <stdio.h>
#include <mem.h>
#include <math.h>
#include <io.h>
#define BUFFER_SIZE 1024*256
#define ADPCMA_VOLUME_RATE 1
#define ADPCMA_DECODE_RANGE 1024
#define ADPCMA_DECODE_MIN (-(ADPCMA_DECODE_RANGE*ADPCMA_VOLUME_RATE))
#define ADPCMA_DECODE_MAX ((ADPCMA_DECODE_RANGE*ADPCMA_VOLUME_RATE)-1)
#define ADPCMA_VOLUME_DIV 1
unsigned char RiffWave[44] = {
0x52, 0x49, 0x46, 0x46, 0x24, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74,
0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0x48, 0x00, 0x00, 0x88, 0x90,
0x00, 0x00, 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00
} ;
static int decode_tableA1[16] = {
-1*16, -1*16, -1*16, -1*16, 2*16, 5*16, 7*16, 9*16,
-1*16, -1*16, -1*16, -1*16, 2*16, 5*16, 7*16, 9*16
};
static int jedi_table[49*16];
static int signal;
static int delta;
void adpcm_init(void);
void adpcm_decode(void *, void *, int);
int Limit(int, int, int);
FILE* errorlog;
int main(int argc, char *argv[])
{
FILE *Fp1, *Fp2;
void *InputBuffer, *OutputBuffer;
int Readed;
unsigned int Filelen;
puts("**** ADPCM to PCM converter v 1.01\n");
if (argc!=3) {
puts("USAGE: adpcm <InputFile.pcm> <OutputFile.wav>");
exit(-1);
}
Fp1 = fopen(argv[1], "rb");
if (Fp1==NULL) {
printf("Could not open inputfile %s\n", argv[1]);
exit(-2);
}
Fp2 = fopen(argv[2], "wb");
if (Fp2==NULL) {
printf("Could not open outputfile %s\n", argv[2]);
exit(-3);
}
errorlog = fopen("error.log", "wb");
InputBuffer = malloc(BUFFER_SIZE);
if (InputBuffer == NULL) {
printf("Could not allocate input buffer. (%d bytes)\n", BUFFER_SIZE);
exit(-4);
}
OutputBuffer = malloc(BUFFER_SIZE*10);
if (OutputBuffer == NULL) {
printf("Could not allocate output buffer. (%d bytes)\n", BUFFER_SIZE*4);
exit(-5);
}
adpcm_init();
Filelen = filelength(fileno(Fp1));
*((unsigned int*)(&RiffWave[4])) = Filelen*4 + 0x2C;
*((unsigned int*)(&RiffWave[0x28])) = Filelen*4;
fwrite(RiffWave, 0x2c, 1, Fp2);
do {
Readed = fread(InputBuffer, 1, BUFFER_SIZE, Fp1);
if (Readed>0) {
adpcm_decode(InputBuffer, OutputBuffer, Readed);
fwrite(OutputBuffer, Readed*4, 1, Fp2);
}
} while (Readed==BUFFER_SIZE);
free(InputBuffer);
free(OutputBuffer);
fclose(Fp1);
fclose(Fp2);
puts("Done...");
return 0;
}
void adpcm_init(void)
{
int step, nib;
for (step = 0; step <= 48; step++)
{
int stepval = floor (16.0 * pow (11.0 / 10.0, (double)step) * ADPCMA_VOLUME_RATE);
/* loop over all nibbles and compute the difference */
for (nib = 0; nib < 16; nib++)
{
int value = stepval*((nib&0x07)*2+1)/8;
jedi_table[step*16+nib] = (nib&0x08) ? -value : value;
}
}
delta = 0;
signal = 0;
}
void adpcm_decode(void *InputBuffer, void *OutputBuffer, int Length)
{
char *in;
short *out;
int i, data, oldsignal;
in = (char *)InputBuffer;
out = (short *)OutputBuffer;
for(i=0;i<Length;i++) {
data = ((*in)>>4)&0x0F;
oldsignal = signal;
signal = Limit(signal + (jedi_table[data+delta]), ADPCMA_DECODE_MAX, ADPCMA_DECODE_MIN);
delta = Limit(delta + decode_tableA1[data], 48*16, 0*16);
if (abs(oldsignal-signal)>2500) {
fprintf(errorlog, "WARNING: Suspicious signal evolution %06x,%06x pos:%06x delta:%06x\n", oldsignal, signal, i, delta);
fprintf(errorlog, "data:%02x dx:%08x\n", data, jedi_table[data+delta]);
}
*(out++) = (signal&0xffff)*32;
data = (*in++)&0x0F;
oldsignal = signal;
signal = Limit(signal + (jedi_table[data+delta]), ADPCMA_DECODE_MAX, ADPCMA_DECODE_MIN);
delta = Limit(delta + decode_tableA1[data], 48*16, 0*16);
if (abs(oldsignal-signal)>2500) {
fprintf(errorlog, "WARNING: Suspicious signal evolution %06x,%06x pos:%06x delta:%06x\n", oldsignal, signal, i, delta);
fprintf(errorlog, "data:%02x dx:%08x\n", data, jedi_table[data+delta]);
}
*(out++) = (signal&0xffff)*32;
}
}
int Limit( int val, int max, int min ) {
if ( val > max )
val = max;
else if ( val < min )
val = min;
return val;
}

View File

@@ -1,16 +0,0 @@
**** ADPCM to PCM converter v 1.01
USAGE: adpcm <InputFile.pcm> <OutputFile.wav>
By MARTINEZ Fabrice aka SNK of SUPREMACY
--(ADPCM.diz.txt)--
ADPCM To WAV Converter
This simple utility converts ADPCM files from NeoGeo cartridge systems
(*_v?.rom) or NeoGeo CD systems (*.PCM). This tool converts ADPCM A type
samples (99% of the samples), ADPCM B type samples will sound distorted...
Get it here

View File

@@ -1,15 +0,0 @@
CC = gcc
CFLAGS = -O3
SRC = .
OBJ = .
OUT_OBJ = $(OBJ)/adpcmb.exe
all: $(OUT_OBJ)
$(OBJ)/%.exe: $(SRC)/%.c
$(CC) $(CCFLAGS) $(MAINFLAGS) $< -o $@
clean:
rm $(OUT_OBJ)

445
adpcmb.c
View File

@@ -1,445 +0,0 @@
/*; YM2610 ADPCM-B Codec
;
;**** PCM to ADPCM-B & ADPCM-B to PCM converters for NEO-GEO System ****
;ADPCM-B - 1 channel 1.8-55.5 KHz, 16 MB Sample ROM size, 256 B min size of sample, 16 MB max, compatable with YM2608
;
;http://www.raregame.ru/file/15/YM2610.pdf YM2610 DATASHEET
;
; Fred/FRONT
;
;Usage 1: ADPCM_Encode.exe -d [-r:reg,clock] Input.bin Output.wav
;Usage 2: ADPCM_Encode.exe -e Input.wav Output.bin
;
; Valley Bell
;----------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#if defined(_MSC_VER)
#define INLINE static __inline
#elif defined(__GNUC__)
#define INLINE static __inline__
#else
#define INLINE static inline
#endif
#ifdef USE_STDINT
#include <stdint.h>
typedef uint8_t UINT8;
typedef int8_t INT8;
typedef uint16_t UINT16;
typedef int16_t INT16;
typedef uint32_t UINT32;
typedef int32_t INT32;
#else
typedef unsigned char UINT8;
typedef signed char INT8;
typedef unsigned short UINT16;
typedef signed short INT16;
typedef unsigned int UINT32;
typedef signed int INT32;
#endif
typedef UINT8 BYTE;
typedef UINT16 WORD;
typedef UINT32 DWORD;
// -- from mmsystem.h --
#define MAKEFOURCC(ch0, ch1, ch2, ch3) \
((DWORD)(BYTE)(ch0) | ((DWORD)(BYTE)(ch1) << 8) | \
((DWORD)(BYTE)(ch2) << 16) | ((DWORD)(BYTE)(ch3) << 24 ))
// -- from mmreg.h, slightly modified --
/* general waveform format structure (information common to all formats) */
typedef struct waveformat_tag {
WORD wFormatTag; /* format type */
WORD nChannels; /* number of channels (i.e. mono, stereo...) */
DWORD nSamplesPerSec; /* sample rate */
DWORD nAvgBytesPerSec; /* for buffer estimation */
WORD nBlockAlign; /* block size of data */
WORD wBitsPerSample;
} WAVEFORMAT;
/* flags for wFormatTag field of WAVEFORMAT */
#define WAVE_FORMAT_PCM 1
// -- from mm*.h end --
#define FOURCC_RIFF MAKEFOURCC('R', 'I', 'F', 'F')
#define FOURCC_WAVE MAKEFOURCC('W', 'A', 'V', 'E')
#define FOURCC_fmt_ MAKEFOURCC('f', 'm', 't', ' ')
#define FOURCC_data MAKEFOURCC('d', 'a', 't', 'a')
typedef struct
{
UINT32 RIFFfcc; // 'RIFF'
UINT32 RIFFLen;
UINT32 WAVEfcc; // 'WAVE'
UINT32 fmt_fcc; // 'fmt '
UINT32 fmt_Len;
WAVEFORMAT fmt_Data;
UINT32 datafcc; // 'data'
UINT32 dataLen;
} WAVE_FILE;
static const long stepsizeTable[16] =
{
57, 57, 57, 57, 77, 102, 128, 153,
57, 57, 57, 57, 77, 102, 128, 153
};
static int YM2610_ADPCM_Encode(INT16 *src, UINT8 *dest, int len)
{
int lpc, flag;
long i, dn, xn, stepSize;
UINT8 adpcm;
UINT8 adpcmPack;
xn = 0;
stepSize = 127;
flag = 0;
for (lpc = 0; lpc < len; lpc ++)
{
dn = *src - xn;
src ++;
i = (abs(dn) << 16) / (stepSize << 14);
if (i > 7)
i = 7;
adpcm = (UINT8)i;
i = (adpcm * 2 + 1) * stepSize / 8;
if (dn < 0)
{
adpcm |= 0x8;
xn -= i;
}
else
{
xn += i;
}
stepSize = (stepsizeTable[adpcm] * stepSize) / 64;
if (stepSize < 127)
stepSize = 127;
else if (stepSize > 24576)
stepSize = 24576;
if (flag == 0)
{
adpcmPack = (adpcm << 4);
flag = 1;
}
else
{
adpcmPack |= adpcm;
*dest = adpcmPack;
dest ++;
flag = 0;
}
}
return 0;
}
static int YM2610_ADPCM_Decode(UINT8 *src, INT16 *dest, int len)
{
int lpc, flag, shift, step;
long i, xn, stepSize;
UINT8 adpcm;
xn = 0;
stepSize = 127;
flag = 0;
shift = 4;
step = 0;
for (lpc = 0; lpc < len; lpc ++)
{
adpcm = (*src >> shift) & 0xf;
i = ((adpcm & 7) * 2 + 1) * stepSize / 8;
if (adpcm & 8)
xn -= i;
else
xn += i;
if (xn > 32767)
xn = 32767;
else if (xn < -32768)
xn = -32768;
stepSize = stepSize * stepsizeTable[adpcm] / 64;
if (stepSize < 127)
stepSize = 127;
else if (stepSize > 24576)
stepSize = 24576;
*dest = (INT16)xn;
dest ++;
src += step;
step = step ^ 1;
shift = shift ^ 4;
}
return 0;
}
INLINE UINT32 DeltaTReg2SampleRate(UINT16 DeltaN, UINT32 Clock)
{
return (UINT32)(DeltaN * (Clock / 72.0) / 65536.0 + 0.5);
}
int main(int argc, char* argv[])
{
int ErrVal;
int ArgBase;
DWORD OutSmplRate;
FILE* hFile;
unsigned int AdpcmSize;
UINT8* AdpcmData;
unsigned int WaveSize;
UINT16* WaveData;
WAVE_FILE WaveFile;
WAVEFORMAT* TempFmt;
unsigned int TempLng;
UINT16 DTRegs;
char* TempPnt;
printf("NeoGeo ADPCM-B En-/Decoder\n--------------------------\n");
if (argc < 4)
{
printf("Usage: ADPCM_Encode.exe -method [-option] InputFile OutputFile\n");
printf("-method - En-/Decoding Method:\n");
printf(" -d for decode (bin -> wav)\n");
printf(" -e for encode (wav -> bin)\n");
printf("-option - Options for Sample Rate of decoded Wave:\n");
printf(" -s:rate - Sample Rate in Hz (default: 22050)\n");
printf(" -r:regs[,clock] - DeltaT Register value (use 0x for hex) and chip clock\n");
printf(" (default chip clock: 4 MHz)\n");
printf("\n");
printf("Wave In-/Output is 16-bit, Mono\n");
return 1;
}
if (strcmp(argv[1], "-d") && strcmp(argv[1], "-e"))
{
printf("Wrong option! Use -d or -e!\n");
return 1;
}
ErrVal = 0;
AdpcmData = NULL;
WaveData = NULL;
OutSmplRate = 0;
ArgBase = 2;
if (argv[2][0] == '-' && argv[2][2] == ':')
{
switch(argv[2][1])
{
case 's':
OutSmplRate = strtol(argv[2] + 3, NULL, 0);
break;
case 'r':
DTRegs = (UINT16)strtoul(argv[2] + 3, &TempPnt, 0);
TempLng = 0;
if (*TempPnt == ',')
{
TempLng = strtoul(TempPnt + 1, NULL, 0);
}
if (! TempLng)
TempLng = 4000000;
OutSmplRate = DeltaTReg2SampleRate(DTRegs, TempLng);
break;
}
ArgBase ++;
if (argc < ArgBase + 2)
{
printf("Not enought arguments!\n");
return 1;
}
}
switch(argv[1][1])
{
case 'd':
hFile = fopen(argv[ArgBase + 0], "rb");
if (hFile == NULL)
{
printf("Error opening input file!\n");
ErrVal = 2;
goto Finish;
}
fseek(hFile, 0x00, SEEK_END);
AdpcmSize = ftell(hFile);
fseek(hFile, 0x00, SEEK_SET);
AdpcmData = (UINT8*)malloc(AdpcmSize);
fread(AdpcmData, 0x01, AdpcmSize, hFile);
fclose(hFile);
WaveSize = AdpcmSize * 2; // 4-bit ADPCM -> 2 values per byte
WaveData = (UINT16*)malloc(WaveSize * 2);
printf("Decoding ...");
YM2610_ADPCM_Decode(AdpcmData, WaveData, WaveSize);
printf(" OK\n");
WaveFile.RIFFfcc = FOURCC_RIFF;
WaveFile.WAVEfcc = FOURCC_WAVE;
WaveFile.fmt_fcc = FOURCC_fmt_;
WaveFile.fmt_Len = sizeof(WAVEFORMAT);
TempFmt = &WaveFile.fmt_Data;
TempFmt->wFormatTag = WAVE_FORMAT_PCM;
TempFmt->nChannels = 1;
TempFmt->wBitsPerSample = 16;
TempFmt->nSamplesPerSec = OutSmplRate ? OutSmplRate : 22050;
TempFmt->nBlockAlign = TempFmt->nChannels * TempFmt->wBitsPerSample / 8;
TempFmt->nAvgBytesPerSec = TempFmt->nBlockAlign * TempFmt->nSamplesPerSec;
WaveFile.datafcc = FOURCC_data;
WaveFile.dataLen = WaveSize * 2;
WaveFile.RIFFLen = 0x04 + 0x08 + WaveFile.fmt_Len + 0x08 + WaveFile.dataLen;
hFile = fopen(argv[ArgBase + 1], "wb");
if (hFile == NULL)
{
printf("Error opening output file!\n");
ErrVal = 3;
goto Finish;
}
fwrite(&WaveFile, sizeof(WAVE_FILE), 0x01, hFile);
fwrite(WaveData, 0x02, WaveSize, hFile);
fclose(hFile);
printf("File written.\n");
break;
case 'e':
hFile = fopen(argv[ArgBase + 0], "rb");
if (hFile == NULL)
{
printf("Error opening input file!\n");
ErrVal = 2;
goto Finish;
}
fread(&WaveFile.RIFFfcc, 0x0C, 0x01, hFile);
if (WaveFile.RIFFfcc != FOURCC_RIFF || WaveFile.WAVEfcc != FOURCC_WAVE)
{
fclose(hFile);
printf("This is no wave file!\n");
ErrVal = 4;
goto Finish;
}
TempLng = fread(&WaveFile.fmt_fcc, 0x04, 0x01, hFile);
fread(&WaveFile.fmt_Len, 0x04, 0x01, hFile);
while(WaveFile.fmt_fcc != FOURCC_fmt_)
{
if (! TempLng) // TempLng == 0 -> EOF reached
{
fclose(hFile);
printf("Error in wave file: Can't find format-tag!\n");
ErrVal = 4;
goto Finish;
}
fseek(hFile, WaveFile.fmt_Len, SEEK_CUR);
TempLng = fread(&WaveFile.fmt_fcc, 0x04, 0x01, hFile);
fread(&WaveFile.fmt_Len, 0x04, 0x01, hFile);
};
TempLng = ftell(hFile) + WaveFile.fmt_Len;
fread(&WaveFile.fmt_Data, sizeof(WAVEFORMAT), 0x01, hFile);
fseek(hFile, TempLng, SEEK_SET);
TempFmt = &WaveFile.fmt_Data;
if (TempFmt->wFormatTag != WAVE_FORMAT_PCM)
{
fclose(hFile);
printf("Error in wave file: Compressed wave file are not supported!\n");
ErrVal = 4;
goto Finish;
}
if (TempFmt->nChannels != 1)
{
fclose(hFile);
printf("Error in wave file: Unsupported number of channels (%hu)!\n", TempFmt->nChannels);
ErrVal = 4;
goto Finish;
}
if (TempFmt->wBitsPerSample != 16)
{
fclose(hFile);
printf("Error in wave file: Only 16-bit waves are supported! (File uses %hu bit)\n", TempFmt->wBitsPerSample);
ErrVal = 4;
goto Finish;
}
TempLng = fread(&WaveFile.datafcc, 0x04, 0x01, hFile);
fread(&WaveFile.dataLen, 0x04, 0x01, hFile);
while(WaveFile.datafcc != FOURCC_data)
{
if (! TempLng) // TempLng == 0 -> EOF reached
{
fclose(hFile);
printf("Error in wave file: Can't find data-tag!\n");
ErrVal = 4;
goto Finish;
}
fseek(hFile, WaveFile.dataLen, SEEK_CUR);
TempLng = fread(&WaveFile.datafcc, 0x04, 0x01, hFile);
fread(&WaveFile.dataLen, 0x04, 0x01, hFile);
};
WaveSize = WaveFile.dataLen / 2;
WaveData = (UINT16*)malloc(WaveSize * 2);
fread(WaveData, 0x02, WaveSize, hFile);
fclose(hFile);
AdpcmSize = WaveSize / 2;
AdpcmData = (UINT8*)malloc(AdpcmSize);
printf("Encoding ...");
YM2610_ADPCM_Encode(WaveData, AdpcmData, WaveSize);
printf(" OK\n");
hFile = fopen(argv[ArgBase + 1], "wb");
if (hFile == NULL)
{
printf("Error opening output file!\n");
ErrVal = 3;
goto Finish;
}
fwrite(AdpcmData, 0x01, AdpcmSize, hFile);
fclose(hFile);
printf("File written.\n");
break;
}
Finish:
free(AdpcmData);
free(WaveData);
return ErrVal;
}

View File

@@ -1,19 +0,0 @@
if ["%~x1"]==[".vgz"] goto vgztopcm else goto vgmtopcm
:vgmtopcm
neoadpcmextract.exe %1
goto pcmtowav
:vgztopcm
copy /y %1 temp.vgm.gz
gzip.exe -d temp.vgm.gz
neoadpcmextract.exe temp.vgm
del temp.vgm
goto pcmtowav
:pcmtowav
for /r %%v in (smpa_*.pcm) do adpcm.exe "%%v" "%%v.wav"
for /r %%v in (smpb_*.pcm) do adpcmb.exe -d "%%v" "%%v.wav"
del "*.pcm"
mkdir "%~n1"
move "*.wav" "%~n1"

8
common/CMakeLists.txt Normal file
View File

@@ -0,0 +1,8 @@
add_library(headers INTERFACE)
add_library(Common::headers ALIAS headers)
target_include_directories(headers INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
add_library(wave endian.h wave.h wave.c wavefile.c)
add_library(Common::wave ALIAS wave)
target_compile_options(wave PRIVATE ${WARNINGS})
target_link_libraries(wave PUBLIC headers)

68
common/endian.h Normal file
View File

@@ -0,0 +1,68 @@
#ifndef COMMON_ENDIAN_H
#define COMMON_ENDIAN_H
#ifdef __APPLE__
# include <machine/endian.h>
#elif defined(__linux__) || defined(__CYGWIN__) || defined(__OpenBSD__)
# include <endian.h>
#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__)
# include <sys/endian.h>
# ifdef __FreeBSD__
# define LITTLE_ENDIAN _LITTLE_ENDIAN
# define BIG_ENDIAN _BIG_ENDIAN
# define BYTE_ORDER _BYTE_ORDER
# endif
#elif defined(_MSC_VER) || defined(_WIN16) || defined(_WIN32) || defined(_WIN64)
# ifdef _MSC_VER
# define LITTLE_ENDIAN 1234
# define BIG_ENDIAN 4321
# if defined(_M_IX86) || defined(_M_X64) || defined(_M_AMD64) || defined(_M_IA64)
# define BYTE_ORDER LITTLE_ENDIAN
# elif defined(_M_PPC)
// Probably not reliable but eh
# define BYTE_ORDER BIG_ENDIAN
# endif
# elif defined(__GNUC__)
# include <sys/param.h>
# endif
#endif
#if !defined(BYTE_ORDER) || !defined(LITTLE_ENDIAN) || !defined(BIG_ENDIAN) || \
!(BYTE_ORDER == LITTLE_ENDIAN || BYTE_ORDER == BIG_ENDIAN)
# error "Couldn't determine endianness or unsupported platform"
#endif
#ifdef _MSC_VER
# define swap32(X) _byteswap_ulong((X))
# define swap16(X) _byteswap_ushort((X))
#elif (__GNUC__ == 4 && __GNUC_MINOR__ >= 8) || (__GNUC__ > 4)
# define swap32(X) __builtin_bswap32((X))
# define swap16(X) __builtin_bswap16((X))
// Apparently smelly GCC 5 blows up on this test so this is done separately for Clang
#elif defined(__has_builtin) && __has_builtin(__builtin_bswap32) && __has_builtin(__builtin_bswap16)
# define swap32(X) __builtin_bswap32((X))
# define swap16(X) __builtin_bswap16((X))
#else
static inline uint32_t swap32(uint32_t v)
{
return (v << 24U) | ((v << 8U) & 0x00FF0000U) | ((v >> 8U) & 0x0000FF00U) | (v >> 24U);
}
static inline uint16_t swap16(uint16_t v)
{
return (v << 8U) | (v >> 8U);
}
#endif
#if BYTE_ORDER == LITTLE_ENDIAN
# define SWAP_LE32(V) (V)
# define SWAP_LE16(V) (V)
# define SWAP_BE32(V) swap32((V))
# define SWAP_BE16(V) swap16((V))
#elif BYTE_ORDER == BIG_ENDIAN
# define SWAP_LE32(V) swap32((V))
# define SWAP_LE16(V) swap16((V))
# define SWAP_BE32(V) (V)
# define SWAP_BE16(V) (V)
#endif
#endif//COMMON_ENDIAN_H

40
common/riffwriter.py Normal file
View File

@@ -0,0 +1,40 @@
# riffwriter.py -- Generic RIFF writing framework
# (C) 2023 a dinosaur (zlib)
from abc import ABC, abstractmethod
from typing import BinaryIO, List
class AbstractRiffChunk(ABC):
@abstractmethod
def fourcc(self) -> bytes: raise NotImplementedError
@abstractmethod
def size(self) -> int: raise NotImplementedError
@abstractmethod
def write(self, f: BinaryIO): raise NotImplementedError
class RiffFile(AbstractRiffChunk):
def fourcc(self) -> bytes: return b"RIFF"
def size(self) -> int: return 4 + sum(8 + c.size() for c in self._chunks)
def __init__(self, type: bytes, chunks: List[AbstractRiffChunk]):
self._chunks = chunks
if len(type) != 4: raise ValueError
self._type = type
def write(self, f: BinaryIO):
f.writelines([
self.fourcc(),
self.size().to_bytes(4, "little", signed=False),
self._type])
for chunk in self._chunks:
size = chunk.size()
if size & 0x3: raise AssertionError("Unaligned chunks will produce malformed riff files")
f.writelines([
chunk.fourcc(),
size.to_bytes(4, "little", signed=False)])
chunk.write(f)

16
common/util.h Normal file
View File

@@ -0,0 +1,16 @@
#ifndef COMMON_UTIL_H
#define COMMON_UTIL_H
#define MIN(A, B) (((A) < (B)) ? (A) : (B))
#define MAX(A, B) (((A) > (B)) ? (A) : (B))
#define CLAMP(X, A, B) (MIN(MAX((X), (A)), (B)))
#if (defined(__GNUC__) && (__GNUC__ >= 4)) || defined(__clang__)
# define FORCE_INLINE inline __attribute__((always_inline))
#elif defined(_MSC_VER)
# define FORCE_INLINE __forceinline
#else
# define FORCE_INLINE inline
#endif
#endif//COMMON_UTIL_H

147
common/wave.c Normal file
View File

@@ -0,0 +1,147 @@
/* wave.c (c) 2023 a dinosaur (zlib) */
#include "wave.h"
#include "endian.h"
#define FOURCC_RIFF "RIFF"
#define FOURCC_WAVE "WAVE"
#define FOURCC_FORM "fmt "
#define FOURCC_SAMP "smpl"
#define FOURCC_DATA "data"
#define FORMAT_CHUNK_SIZE 16
typedef struct
{
uint16_t format;
uint16_t channels;
uint32_t samplerate;
uint32_t byterate;
uint16_t alignment;
uint16_t bitdepth;
} FormatChunk;
typedef struct
{
uint32_t id;
uint32_t type;
uint32_t loopstart;
uint32_t loopend;
uint32_t fraction;
uint32_t playcount;
} SampleLoopChunk;
#define SMPL_CHUNK_HEAD_SIZE 24
typedef struct
{
uint32_t manufacturer;
uint32_t product;
uint32_t samplePeriod;
uint32_t midiUniNote;
uint32_t midiPitchFrac;
uint32_t smpteFormat;
uint32_t smpteOffset;
uint32_t sampleLoopCount;
uint32_t sampleData;
} SamplerChunk;
static void writeFourcc(const WaveStreamCb* restrict cb, void* restrict user, const char fourcc[4])
{
cb->write(user, fourcc, 1, 4);
}
static void writeU32le(const WaveStreamCb* restrict cb, void* restrict user, uint32_t v)
{
uint32_t tmp = SWAP_LE32(v);
cb->write(user, &tmp, sizeof(uint32_t), 1);
}
static void writeU16le(const WaveStreamCb* restrict cb, void* restrict user, uint16_t v)
{
uint16_t tmp = SWAP_LE16(v);
cb->write(user, &tmp, sizeof(uint16_t), 1);
}
void writeRiffChunk(const WaveStreamCb* restrict cb, void* restrict user, char fourcc[4], uint32_t size)
{
writeFourcc(cb, user, fourcc);
writeU32le(cb, user, size);
}
void writeFormatChunk(const WaveStreamCb* restrict cb, void* restrict user, const FormatChunk* fmt)
{
writeRiffChunk(cb, user, FOURCC_FORM, FORMAT_CHUNK_SIZE);
writeU16le(cb, user, fmt->format);
writeU16le(cb, user, fmt->channels);
writeU32le(cb, user, fmt->samplerate);
writeU32le(cb, user, fmt->byterate);
writeU16le(cb, user, fmt->alignment);
writeU16le(cb, user, fmt->bitdepth);
}
static int waveWriteHeader(const WaveSpec* spec, size_t dataLen, const WaveStreamCb* cb, void* user)
{
if (!spec || !dataLen || dataLen >= UINT32_MAX || !cb || !cb->write)
return 1;
if (spec->format != WAVE_FMT_PCM)
return 1;
if (spec->channels <= 0 || spec->channels >= INT16_MAX)
return 1;
if (spec->format == WAVE_FMT_PCM && (spec->bytedepth <= 0 || spec->bytedepth > 4))
return 1;
// write riff container
writeRiffChunk(cb, user, FOURCC_RIFF, sizeof(uint32_t) * 5 + FORMAT_CHUNK_SIZE + (uint32_t)dataLen);
writeFourcc(cb, user, FOURCC_WAVE);
writeFormatChunk(cb, user, &(const FormatChunk)
{
.format = spec->format,
.channels = (uint16_t)spec->channels,
.samplerate = spec->rate,
.byterate = spec->rate * spec->channels * spec->bytedepth,
.alignment = (uint16_t)(spec->channels * spec->bytedepth),
.bitdepth = (uint16_t)spec->bytedepth * 8
});
// write data chunk
writeFourcc(cb, user, FOURCC_DATA);
writeU32le(cb, user, (uint32_t)dataLen);
return 0;
}
int waveWrite(const WaveSpec* spec, const void* data, size_t dataLen, const WaveStreamCb* cb, void* user)
{
// Write RIFF/Wave header and raw interleaved samples
int res = waveWriteHeader(spec, dataLen, cb, user);
if (res)
return res;
//FIXME: not endian safe
if (data)
cb->write(user, data, 1, dataLen);
return 0;
}
int waveWriteBlock(const WaveSpec* spec, const void* blocks[], size_t blockLen, const WaveStreamCb* cb, void* user)
{
if (!blocks)
return 1;
// Write RIFF/Wave header to file
int res = waveWriteHeader(spec, blockLen * spec->channels, cb, user);
if (res)
return res;
// Copy & interleave
//FIXME: not endian safe
for (size_t i = 0; i < blockLen / spec->bytedepth; ++i)
for (int j = 0; j < spec->channels; ++j)
cb->write(user, &((const char**)blocks)[j][i * spec->bytedepth], spec->bytedepth, 1);
return 0;
}

48
common/wave.h Normal file
View File

@@ -0,0 +1,48 @@
#ifndef WAVE_H
#define WAVE_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
typedef enum
{
WAVE_FMT_PCM = 0x0001,
WAVE_FMT_IEEE_FLOAT = 0x0003,
WAVE_FMT_ALAW = 0x0006,
WAVE_FMT_MULAW = 0x0007,
WAVE_FMT_EXTENSIBLE = 0xFFFE
} WaveFormat;
typedef struct
{
WaveFormat format;
int channels;
unsigned rate;
int bytedepth;
} WaveSpec;
typedef struct
{
size_t (*read)(void* restrict user, void* restrict out, size_t size, size_t num);
size_t (*write)(void* restrict user, const void* restrict src, size_t size, size_t num);
void (*seek)(void* restrict user, int offset);
size_t (*tell)(void* user);
int (*eof)(void* user);
} WaveStreamCb;
extern const WaveStreamCb waveStreamDefaultCb;
int waveWrite(const WaveSpec* spec, const void* data, size_t dataLen, const WaveStreamCb* cb, void* user);
int waveWriteFile(const WaveSpec* spec, const void* data, size_t dataLen, const char* path);
int waveWriteBlock(const WaveSpec* spec, const void* blocks[], size_t blockLen, const WaveStreamCb* cb, void* user);
int waveWriteBlockFile(const WaveSpec* spec, const void* blocks[], size_t blockLen, const char* path);
#ifdef __cplusplus
}
#endif
#endif//WAVE_H

64
common/wavefile.c Normal file
View File

@@ -0,0 +1,64 @@
/* wavefile.c (c) 2023 a dinosaur (zlib) */
#include "wave.h"
#include <stdio.h>
static size_t waveFileRead(void* restrict user, void* restrict out, size_t size, size_t num)
{
return fread(out, size, num, (FILE*)user);
}
static size_t waveFileWrite(void* restrict user, const void* restrict src, size_t size, size_t num)
{
return fwrite(src, size, num, (FILE*)user);
}
static void waveFileSeek(void* restrict user, int offset)
{
#ifndef _MSC_VER
fseek((FILE*)user, (off_t)offset, SEEK_CUR);
#else
fseek((FILE*)user, (long)offset, SEEK_CUR);
#endif
}
static size_t waveFileTell(void* user)
{
return ftell((FILE*)user);
}
static int waveFileEof(void* user)
{
return feof((FILE*)user) || ferror((FILE*)user);
}
const WaveStreamCb waveStreamDefaultCb =
{
.read = waveFileRead,
.write = waveFileWrite,
.seek = waveFileSeek,
.tell = waveFileTell,
.eof = waveFileEof
};
int waveWriteFile(const WaveSpec* spec, const void* data, size_t dataLen, const char* path)
{
FILE* file = fopen(path, "wb");
if (!file)
return 1;
int res = waveWrite(spec, data, dataLen, &waveStreamDefaultCb, (void*)file);
fclose(file);
return res;
}
int waveWriteBlockFile(const WaveSpec* spec, const void* blocks[], size_t blockLen, const char* path)
{
FILE* file = fopen(path, "wb");
if (!file)
return 1;
int res = waveWriteBlock(spec, blocks, blockLen, &waveStreamDefaultCb, (void*)file);
fclose(file);
return res;
}

110
common/wavesampler.py Normal file
View File

@@ -0,0 +1,110 @@
# wavesampler.py -- Support for the non-standard "Sampler" WAVE chunk
# (C) 2023 a dinosaur (zlib)
import struct
from enum import Enum
from typing import BinaryIO, List
from common.riffwriter import AbstractRiffChunk
class WaveSamplerSMPTEOffset:
def __init__(self, hours: int=0, minutes: int=0, seconds: int=0, frames: int=0):
if -23 > hours > 23: raise ValueError("Hours out of range")
if 0 > minutes > 59: raise ValueError("Minutes out of range")
if 0 > seconds > 59: raise ValueError("Seconds out of range")
if 0 > frames > 0xFF: raise ValueError("Frames out of range")
self._hours = hours
self._minutes = minutes
self._seconds = seconds
self._frames = frames
def hours(self) -> int: return self._hours
def minutes(self) -> int: return self._minutes
def seconds(self) -> int: return self._seconds
def frames(self) -> int: return self._frames
def pack(self) -> bytes:
#FIXME: endianess??
return struct.pack("<bBBB", self._hours, self._minutes, self._seconds, self._frames)
class WaveSamplerLoopType(Enum):
FORWARD = 0
BIDIRECTIONAL = 1
REVERSE = 2
class WaveSamplerLoop:
def __init__(self,
cueId: int=0,
type: int|WaveSamplerLoopType=0,
start: int=0,
end: int=0,
fraction: int=0,
loopCount: int=0):
self._cueId = cueId
self._type = type.value if type is WaveSamplerLoopType else type
self._start = start
self._end = end
self._fraction = fraction
self._loopCount = loopCount
def pack(self) -> bytes:
return struct.pack("<IIIIII",
self._cueId, # Cue point ID
self._type, # Loop type
self._start, # Loop start
self._end, # Loop end
self._fraction, # Fraction (none)
self._loopCount) # Loop count (infinite)
class WaveSamplerChunk(AbstractRiffChunk):
def fourcc(self) -> bytes: return b"smpl"
def loopsSize(self) -> int: return len(self._loops) * 24
def size(self) -> int: return 36 + self.loopsSize()
def write(self, f: BinaryIO):
#TODO: unused data dummied out for now
f.write(struct.pack("<4sIiiii4sII",
self._manufacturer, # MMA Manufacturer code
self._product, # Product
self._period, # Playback period (ns)
self._unityNote, # MIDI unity note
self._fineTune, # MIDI pitch fraction
self._smpteFormat, # SMPTE format
self._smpteOffset.pack(), # SMPTE offset
len(self._loops), # Number of loops
self.loopsSize())) # Loop data length
f.writelines(loop.pack() for loop in self._loops)
def __init__(self,
manufacturer: bytes|None=None,
product: int=0,
period: int=0,
midiUnityNote: int=0,
midiPitchFraction: int=0,
smpteFormat: int=0,
smpteOffset: WaveSamplerSMPTEOffset|None=None,
loops: List[WaveSamplerLoop]=None):
if manufacturer is not None:
if len(manufacturer) not in [1, 3]: raise ValueError("Malformed MIDI manufacturer code")
self._manufacturer = len(manufacturer).to_bytes(1, byteorder="little", signed=False)
self._manufacturer += manufacturer.rjust(3, b"\x00")
else:
self._manufacturer = b"\x00" * 4
if 0 > product > 0xFFFF: raise ValueError("Product code out of range")
self._product = product # Arbitrary vendor specific product code, dunno if this should be signed or unsigned
self._period = period # Sample period in ns, (1 / samplerate) * 10^9, who cares
if 0 > midiUnityNote > 127: raise ValueError("MIDI Unity note out of range")
self._unityNote = midiUnityNote # MIDI note that plays the sample unpitched, middle C=60
self._fineTune = midiPitchFraction # Finetune fraction, 256 == 100 cents
if smpteFormat not in [0, 24, 25, 29, 30]: raise ValueError("Invalid SMPTE format")
self._smpteFormat = smpteFormat
self._smpteOffset = smpteOffset if (smpteOffset and smpteFormat > 0) else WaveSamplerSMPTEOffset()
if self._smpteOffset.frames() > self._smpteFormat: raise ValueError("SMPTE frame offset can't exceed SMPTE format")
self._loops = loops if loops is not None else list()

18
common/waveserum.py Normal file
View File

@@ -0,0 +1,18 @@
# waveserum.py -- Serum comment chunk support
# (C) 2023 a dinosaur (zlib)
from enum import Enum
from common.wavewriter import WaveCommentChunk
class SerumWavetableInterpolation(Enum):
NONE = 0
LINEAR_XFADE = 1
SPECTRAL_MORPH = 2
class WaveSerumCommentChunk(WaveCommentChunk):
def __init__(self, size: int, mode: SerumWavetableInterpolation, factory=False):
comment = f"<!>{size: <4} {mode.value}{'1' if factory else '0'}000000"
comment += " wavetable (www.xferrecords.com)"
super().__init__(comment.encode("ascii"))

98
common/wavewriter.py Normal file
View File

@@ -0,0 +1,98 @@
# wavewriter.py -- Extensible WAVE writing framework
# (C) 2023 a dinosaur (zlib)
import struct
from abc import abstractmethod
from enum import Enum
from typing import BinaryIO, List
from common.riffwriter import RiffFile, AbstractRiffChunk
class WaveSampleFormat(Enum):
PCM = 0x0001
IEEE_FLOAT = 0x0003
ALAW = 0x0006
MULAW = 0x0007
EXTENSIBLE = 0xFFFE
class WaveAbstractFormatChunk(AbstractRiffChunk):
def fourcc(self) -> bytes: return b"fmt "
def size(self) -> int: return 16
@abstractmethod
def sampleformat(self) -> WaveSampleFormat: raise NotImplementedError
@abstractmethod
def channels(self) -> int: raise NotImplementedError
@abstractmethod
def samplerate(self) -> int: raise NotImplementedError
@abstractmethod
def byterate(self) -> int: raise NotImplementedError
@abstractmethod
def align(self) -> int: raise NotImplementedError
@abstractmethod
def bitdepth(self) -> int: raise NotImplementedError
def write(self, f: BinaryIO):
f.write(struct.pack("<HHIIHH",
self.sampleformat().value,
self.channels(),
self.samplerate(),
self.byterate(),
self.align(),
self.bitdepth()))
class WaveFile(RiffFile):
def __init__(self, format: WaveAbstractFormatChunk, chunks: List[AbstractRiffChunk]):
super().__init__(b"WAVE", [format] + chunks)
class WavePcmFormatChunk(WaveAbstractFormatChunk):
def sampleformat(self) -> WaveSampleFormat: return WaveSampleFormat.PCM
def channels(self) -> int: return self._channels
def samplerate(self) -> int: return self._samplerate
def byterate(self) -> int: return self._samplerate * self._channels * self._bytedepth
def align(self) -> int: return self._channels * self._bytedepth
def bitdepth(self) -> int: return self._bytedepth * 8
def __init__(self, channels: int, samplerate: int, bitdepth: int):
if channels < 0 or channels >= 256: raise ValueError
if samplerate < 1 or samplerate > 0xFFFFFFFF: raise ValueError
if bitdepth not in [8, 16, 32]: raise ValueError
self._channels = channels
self._samplerate = samplerate
self._bytedepth = bitdepth // 8
class WaveDataChunk(AbstractRiffChunk):
def fourcc(self) -> bytes: return b"data"
def size(self) -> int: return len(self._data)
def write(self, f: BinaryIO): f.write(self._data)
def __init__(self, data: bytes):
self._data = data
class WaveCommentChunk(AbstractRiffChunk):
def fourcc(self) -> bytes: return b"clm "
def size(self) -> int: return len(self._comment)
def write(self, f: BinaryIO): f.write(self._comment)
def __init__(self, comment: bytes):
self._comment = comment

7
dsptools/CMakeLists.txt Normal file
View File

@@ -0,0 +1,7 @@
add_subdirectory(libdsptool)
add_executable(dspdecode dspdecode.c)
set_property(TARGET dspdecode PROPERTY C_STANDARD 99)
target_include_directories(dspdecode PRIVATE ${COMMON})
target_link_libraries(dspdecode Common::wave DspTool::DspTool)
target_compile_options(dspdecode PRIVATE ${WARNINGS})

261
dsptools/dspdecode.c Normal file
View File

@@ -0,0 +1,261 @@
/* dspdecode.c (c) 2023 a dinosaur (zlib) */
#include "dsptool.h"
#include "wave.h"
#include "endian.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
typedef struct
{
uint32_t numSamples;
uint32_t numNibbles;
uint32_t sampleRate;
uint16_t loopFlag;
uint16_t format; // Reserved, always 0
uint32_t loopBeg;
uint32_t loopEnd;
uint32_t curAddress; // Reserved, always 0
int16_t coefs[16];
uint16_t gain; // Always 0
uint16_t predScale;
int16_t history[2];
uint16_t loopPredScale;
int16_t loopHistory[2];
int16_t channels;
uint16_t blockSize;
uint16_t reserved1[9];
} DspHeader;
static void readBeU32(uint32_t* restrict dst, size_t count, FILE* restrict file)
{
fread(dst, sizeof(uint32_t), count, file);
#if BYTE_ORDER == LITTLE_ENDIAN
for (size_t i = 0; i < count; ++i)
dst[i] = swap32(dst[i]);
#endif
}
static void readBeU16(uint16_t* restrict dst, size_t count, FILE* restrict file)
{
fread(dst, sizeof(uint16_t), count, file);
#if BYTE_ORDER == LITTLE_ENDIAN
for (size_t i = 0; i < count; ++i)
dst[i] = swap16(dst[i]);
#endif
}
static void readBeS16(int16_t* restrict dst, size_t count, FILE* restrict file)
{
fread(dst, sizeof(int16_t), count, file);
#if BYTE_ORDER == LITTLE_ENDIAN
for (size_t i = 0; i < count; ++i)
dst[i] = (int16_t)swap16((uint16_t)dst[i]);
#endif
}
typedef struct
{
int16_t* pcm;
size_t pcmSize;
uint32_t rate;
} PcmFile;
#define PCMFILE_CLEAR() (PcmFile){ NULL, 0, 0 }
static int loadDsp(const char* path, PcmFile* out)
{
FILE* file = fopen(path, "rb");
if (!file)
{
fprintf(stderr, "File not found\n");
return 1;
}
uint8_t* adpcm = NULL;
out->pcm = NULL;
DspHeader dsp;
readBeU32(&dsp.numSamples, 1, file);
readBeU32(&dsp.numNibbles, 1, file);
readBeU32(&dsp.sampleRate, 1, file);
readBeU16(&dsp.loopFlag, 1, file);
readBeU16(&dsp.format, 1, file);
readBeU32(&dsp.loopBeg, 1, file);
readBeU32(&dsp.loopEnd, 1, file);
readBeU32(&dsp.curAddress, 1, file);
readBeS16(dsp.coefs, 16, file);
readBeU16(&dsp.gain, 1, file);
readBeU16(&dsp.predScale, 1, file);
readBeS16(dsp.history, 2, file);
readBeU16(&dsp.loopPredScale, 1, file);
readBeS16(dsp.loopHistory, 2, file);
readBeS16(&dsp.channels, 1, file);
readBeU16(&dsp.blockSize, 1, file);
readBeU16(dsp.reserved1, 9, file);
if (dsp.loopFlag > 1 || dsp.format)
goto Fail;
size_t adpcmSize = getBytesForAdpcmBuffer(dsp.numSamples);
adpcm = malloc(adpcmSize);
if (!adpcm)
goto Fail;
out->pcmSize = getBytesForPcmBuffer(dsp.numSamples);
out->pcm = malloc(out->pcmSize * sizeof(int16_t));
if (!out->pcm)
goto Fail;
fread(adpcm, 1, adpcmSize, file);
fclose(file);
file = NULL;
ADPCMINFO adpcmInfo =
{
.gain = dsp.gain,
.pred_scale = dsp.predScale,
.yn1 = dsp.history[0],
.yn2 = dsp.history[1],
.loop_pred_scale = dsp.loopPredScale,
.loop_yn1 = dsp.loopHistory[0],
.loop_yn2 = dsp.loopHistory[1]
};
memcpy(adpcmInfo.coef, dsp.coefs, sizeof(int16_t) * 16);
decode(adpcm, out->pcm, &adpcmInfo, dsp.numSamples);
free(adpcm);
out->rate = dsp.sampleRate;
return 0;
Fail:
free(out->pcm);
free(adpcm);
if (file)
fclose(file);
return 1;
}
static void usage(const char* argv0)
{
fprintf(stderr, "Usage: %s <in.dsp> [inR.dsp] [-o out.wav]\n", argv0);
exit(1);
}
int main(int argc, char* argv[])
{
// Parse cli arguments
char* inPathL = NULL, * inPathR = NULL, * outPath = NULL;
bool opt = false, outPathAlloc = false;
for (int i = 1; i < argc; ++i)
{
if (opt)
{
if (outPath)
usage(argv[0]);
outPath = argv[i];
opt = false;
}
else if (argv[i][0] == '-')
{
if (argv[i][1] != 'o')
usage(argv[0]);
opt = true;
}
else if (!inPathL)
{
inPathL = argv[i];
}
else if (!inPathR)
{
inPathR = argv[i];
}
else
{
usage(argv[0]);
}
}
if (opt || !inPathL)
usage(argv[0]);
// Compute output path if one wasn't specified
if (!outPath)
{
const char* base = strrchr(inPathL, '/');
#ifdef _WIN32
const char* base2 = strrchr(inPathL, '\\');
if (base2 && base2 > base)
base = base2;
#endif
if (!base)
base = inPathL;
char* ext = strrchr(base, '.');
size_t nameLen = strlen(inPathL);
if (ext)
nameLen -= strlen(ext);
outPath = malloc(nameLen + 5);
if (!outPath)
return 2;
memcpy(outPath, inPathL, nameLen);
memcpy(outPath + nameLen, ".wav", 5);
outPathAlloc = true;
}
// Convert left (and optionally right) channels to PCM, save as wave
int ret;
PcmFile left = PCMFILE_CLEAR(), right = PCMFILE_CLEAR();
if ((ret = loadDsp(inPathL, &left)))
goto Cleanup;
if (inPathR)
{
if ((ret = loadDsp(inPathR, &right)))
goto Cleanup;
if (left.pcmSize != right.pcmSize || left.rate != right.rate)
{
fprintf(stderr, "Rate or length mismatch");
ret = 1;
goto Cleanup;
}
WaveSpec wav =
{
.format = WAVE_FMT_PCM,
.channels = 2,
.rate = left.rate,
.bytedepth = sizeof(int16_t)
};
if ((ret = waveWriteBlockFile(&wav, (const void*[2]){left.pcm, right.pcm}, left.pcmSize, outPath)))
goto Cleanup;
free(right.pcm);
right = PCMFILE_CLEAR();
}
else
{
WaveSpec wav =
{
.format = WAVE_FMT_PCM,
.channels = 1,
.rate = left.rate,
.bytedepth = sizeof(int16_t)
};
if ((ret = waveWriteFile(&wav, left.pcm, left.pcmSize, outPath)))
goto Cleanup;
}
free(left.pcm);
left = PCMFILE_CLEAR();
ret = 0;
Cleanup:
free(right.pcm);
free(left.pcm);
if (outPathAlloc)
free(outPath);
return ret;
}

View File

@@ -0,0 +1,18 @@
option(DSPTOOL_BUILD_SHARED_LIBS "Build as a Shared Object or DLL" OFF)
set(HEADERS dsptool.h)
set(SOURCES math.c decode.c encode.c)
if (DSPTOOL_BUILD_SHARED_LIBS)
add_library(DspTool SHARED ${HEADERS} ${SOURCES})
target_compile_definitions(DspTool PRIVATE BUILD_SHARED)
else()
add_library(DspTool STATIC ${HEADERS} ${SOURCES})
target_compile_definitions(DspTool PRIVATE BUILD_STATIC)
endif()
add_library(DspTool::DspTool ALIAS DspTool)
set_property(TARGET DspTool PROPERTY C_STANDARD 99)
target_include_directories(DspTool PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(DspTool PRIVATE ${WARNINGS})
target_link_libraries(DspTool PRIVATE Common::headers)

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Alex Barney
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,101 @@
/* (c) 2017 Alex Barney (MIT) */
#include <stdint.h>
#include "util.h"
#include "dsptool.h"
static inline uint8_t GetHighNibble(uint8_t value)
{
return value >> 4 & 0xF;
}
static inline uint8_t GetLowNibble(uint8_t value)
{
return value & 0xF;
}
static inline int16_t Clamp16(int value)
{
if (value > INT16_MAX)
return INT16_MAX;
if (value < INT16_MIN)
return INT16_MIN;
return (int16_t)value;
}
void decode(uint8_t* src, int16_t* dst, ADPCMINFO* cxt, uint32_t samples)
{
short hist1 = cxt->yn1;
short hist2 = cxt->yn2;
short* coefs = cxt->coef;
uint32_t frameCount = (samples + SAMPLES_PER_FRAME - 1) / SAMPLES_PER_FRAME;
uint32_t samplesRemaining = samples;
for (uint32_t i = 0; i < frameCount; i++)
{
int predictor = GetHighNibble(*src);
int scale = 1 << GetLowNibble(*src++);
short coef1 = coefs[predictor * 2];
short coef2 = coefs[predictor * 2 + 1];
uint32_t samplesToRead = MIN(SAMPLES_PER_FRAME, samplesRemaining);
for (uint32_t s = 0; s < samplesToRead; s++)
{
int sample = (s & 0x1)
? GetLowNibble(*src++)
: GetHighNibble(*src);
sample = sample >= 8 ? sample - 16 : sample;
sample = (scale * sample) << 11;
sample = (sample + 1024 + (coef1 * hist1 + coef2 * hist2)) >> 11;
short finalSample = Clamp16(sample);
hist2 = hist1;
hist1 = finalSample;
*dst++ = finalSample;
}
samplesRemaining -= samplesToRead;
}
}
void getLoopContext(uint8_t* src, ADPCMINFO* cxt, uint32_t samples)
{
short hist1 = cxt->yn1;
short hist2 = cxt->yn2;
short* coefs = cxt->coef;
uint8_t ps = 0;
int frameCount = (int)((samples + SAMPLES_PER_FRAME - 1) / SAMPLES_PER_FRAME);
uint32_t samplesRemaining = samples;
for (int i = 0; i < frameCount; i++)
{
ps = *src;
int predictor = GetHighNibble(*src);
int scale = 1 << GetLowNibble(*src++);
short coef1 = coefs[predictor * 2];
short coef2 = coefs[predictor * 2 + 1];
uint32_t samplesToRead = MIN(SAMPLES_PER_FRAME, samplesRemaining);
for (uint32_t s = 0; s < samplesToRead; s++)
{
int sample = (s & 0x1)
? GetLowNibble(*src++)
: GetHighNibble(*src);
sample = sample >= 8 ? sample - 16 : sample;
sample = (scale * sample) << 11;
sample = (sample + 1024 + (coef1 * hist1 + coef2 * hist2)) >> 11;
short finalSample = Clamp16(sample);
hist2 = hist1;
hist1 = finalSample;
}
samplesRemaining -= samplesToRead;
}
cxt->loop_pred_scale = ps;
cxt->loop_yn1 = hist1;
cxt->loop_yn2 = hist2;
}

View File

@@ -0,0 +1,61 @@
#ifndef DSPTOOL_H
#define DSPTOOL_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#define BYTES_PER_FRAME 8
#define SAMPLES_PER_FRAME 14
#define NIBBLES_PER_FRAME 16
#if defined( _WIN32 ) || defined( __CYGWIN__ )
# ifdef BUILD_SHARED
# define DLLEXPORT __declspec(dllexport)
# elif !defined( BUILD_STATIC )
# define DLLEXPORT __declspec(dllimport)
# else
# define DLLEXPORT
# endif
#elif __GNUC__ >= 4
# define DLLEXPORT __attribute__((visibility("default")))
#else
# define DLLEXPORT
#endif
typedef struct
{
int16_t coef[16];
uint16_t gain;
uint16_t pred_scale;
int16_t yn1;
int16_t yn2;
uint16_t loop_pred_scale;
int16_t loop_yn1;
int16_t loop_yn2;
} ADPCMINFO;
DLLEXPORT void encode(int16_t* src, uint8_t* dst, ADPCMINFO* cxt, uint32_t samples);
DLLEXPORT void decode(uint8_t* src, int16_t* dst, ADPCMINFO* cxt, uint32_t samples);
DLLEXPORT void getLoopContext(uint8_t* src, ADPCMINFO* cxt, uint32_t samples);
DLLEXPORT void encodeFrame(int16_t* src, uint8_t* dst, int16_t* coefs, uint8_t one);
DLLEXPORT void correlateCoefs(int16_t* src, uint32_t samples, int16_t* coefsOut);
DLLEXPORT uint32_t getBytesForAdpcmBuffer(uint32_t samples);
DLLEXPORT uint32_t getBytesForAdpcmSamples(uint32_t samples);
DLLEXPORT uint32_t getBytesForPcmBuffer(uint32_t samples);
DLLEXPORT uint32_t getBytesForPcmSamples(uint32_t samples);
DLLEXPORT uint32_t getNibbleAddress(uint32_t samples);
DLLEXPORT uint32_t getNibblesForNSamples(uint32_t samples);
DLLEXPORT uint32_t getSampleForAdpcmNibble(uint32_t nibble);
DLLEXPORT uint32_t getBytesForAdpcmInfo(void);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,558 @@
/* (c) 2017 Alex Barney (MIT) */
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#include <float.h>
#include <string.h>
#include "util.h"
#include "dsptool.h"
/* Temporal Vector
* A contiguous history of 3 samples starting with
* 'current' and going 2 backwards
*/
typedef double tvec[3];
static void DSPEncodeFrame(short pcmInOut[16], int sampleCount, unsigned char adpcmOut[8], const short coefsIn[8][2]);
void encode(int16_t* src, uint8_t* dst, ADPCMINFO* cxt, uint32_t samples)
{
int16_t* coefs = cxt->coef;
correlateCoefs(src, samples, coefs);
uint32_t frameCount = samples / SAMPLES_PER_FRAME + (samples % SAMPLES_PER_FRAME != 0);
int16_t* pcm = src;
uint8_t* adpcm = dst;
int16_t pcmFrame[SAMPLES_PER_FRAME + 2] = { 0 };
uint8_t adpcmFrame[BYTES_PER_FRAME] = { 0 };
for (uint32_t i = 0; i < frameCount; ++i, pcm += SAMPLES_PER_FRAME, adpcm += BYTES_PER_FRAME)
{
uint32_t sampleCount = MIN(samples - i * SAMPLES_PER_FRAME, SAMPLES_PER_FRAME);
memset(pcmFrame + 2, 0, SAMPLES_PER_FRAME * sizeof(int16_t));
memcpy(pcmFrame + 2, pcm, sampleCount * sizeof(int16_t));
DSPEncodeFrame(pcmFrame, SAMPLES_PER_FRAME, adpcmFrame, (const short(*)[2])&coefs[0]);
pcmFrame[0] = pcmFrame[14];
pcmFrame[1] = pcmFrame[15];
memcpy(adpcm, adpcmFrame, getBytesForAdpcmSamples(sampleCount));
}
cxt->gain = 0;
cxt->pred_scale = *dst;
cxt->yn1 = 0;
cxt->yn2 = 0;
}
static void InnerProductMerge(tvec vecOut, const short pcmBuf[14])
{
for (int i = 0; i <= 2; i++)
{
vecOut[i] = 0.0f;
for (int x = 0; x < 14; x++)
vecOut[i] -= pcmBuf[x - i] * pcmBuf[x];
}
}
static void OuterProductMerge(tvec mtxOut[3], const short pcmBuf[14])
{
for (int x = 1; x <= 2; x++)
for (int y = 1; y <= 2; y++)
{
mtxOut[x][y] = 0.0;
for (int z = 0; z < 14; z++)
mtxOut[x][y] += pcmBuf[z - x] * pcmBuf[z - y];
}
}
static bool AnalyzeRanges(tvec mtx[3], int* vecIdxsOut)
{
double recips[3];
double val, tmp, min, max;
// Get greatest distance from zero
for (int x = 1; x <= 2; x++)
{
val = MAX(fabs(mtx[x][1]), fabs(mtx[x][2]));
if (val < DBL_EPSILON)
return true;
recips[x] = 1.0 / val;
}
int maxIndex = 0;
for (int i = 1; i <= 2; i++)
{
for (int x = 1; x < i; x++)
{
tmp = mtx[x][i];
for (int y = 1; y < x; y++)
tmp -= mtx[x][y] * mtx[y][i];
mtx[x][i] = tmp;
}
val = 0.0;
for (int x = i; x <= 2; x++)
{
tmp = mtx[x][i];
for (int y = 1; y < i; y++)
tmp -= mtx[x][y] * mtx[y][i];
mtx[x][i] = tmp;
tmp = fabs(tmp) * recips[x];
if (tmp >= val)
{
val = tmp;
maxIndex = x;
}
}
if (maxIndex != i)
{
for (int y = 1; y <= 2; y++)
{
tmp = mtx[maxIndex][y];
mtx[maxIndex][y] = mtx[i][y];
mtx[i][y] = tmp;
}
recips[maxIndex] = recips[i];
}
vecIdxsOut[i] = maxIndex;
if (mtx[i][i] == 0.0)
return true;
if (i != 2)
{
tmp = 1.0 / mtx[i][i];
for (int x = i + 1; x <= 2; x++)
mtx[x][i] *= tmp;
}
}
// Get range
min = 1.0e10;
max = 0.0;
for (int i = 1; i <= 2; i++)
{
tmp = fabs(mtx[i][i]);
if (tmp < min)
min = tmp;
if (tmp > max)
max = tmp;
}
if (min / max < 1.0e-10)
return true;
return false;
}
static void BidirectionalFilter(tvec mtx[3], const int* vecIdxs, tvec vecOut)
{
double tmp;
for (int i = 1, x = 0; i <= 2; i++)
{
int index = vecIdxs[i];
tmp = vecOut[index];
vecOut[index] = vecOut[i];
if (x != 0)
for (int y = x; y <= i - 1; y++)
tmp -= vecOut[y] * mtx[i][y];
else if (tmp != 0.0)
x = i;
vecOut[i] = tmp;
}
for (int i = 2; i > 0; i--)
{
tmp = vecOut[i];
for (int y = i + 1; y <= 2; y++)
tmp -= vecOut[y] * mtx[i][y];
vecOut[i] = tmp / mtx[i][i];
}
vecOut[0] = 1.0;
}
static bool QuadraticMerge(tvec inOutVec)
{
double v0, v1, v2 = inOutVec[2];
double tmp = 1.0 - (v2 * v2);
if (tmp == 0.0)
return true;
v0 = (inOutVec[0] - (v2 * v2)) / tmp;
v1 = (inOutVec[1] - (inOutVec[1] * v2)) / tmp;
inOutVec[0] = v0;
inOutVec[1] = v1;
return fabs(v1) > 1.0;
}
static void FinishRecord(tvec in, tvec out)
{
for (int z = 1; z <= 2; z++)
{
if (in[z] >= 1.0)
in[z] = 0.9999999999;
else if (in[z] <= -1.0)
in[z] = -0.9999999999;
}
out[0] = 1.0;
out[1] = (in[2] * in[1]) + in[1];
out[2] = in[2];
}
static void MatrixFilter(const tvec src, tvec dst)
{
tvec mtx[3];
mtx[2][0] = 1.0;
for (int i = 1; i <= 2; i++)
mtx[2][i] = -src[i];
for (int i = 2; i > 0; i--)
{
double val = 1.0 - (mtx[i][i] * mtx[i][i]);
for (int y = 1; y <= i; y++)
mtx[i - 1][y] = ((mtx[i][i] * mtx[i][y]) + mtx[i][y]) / val;
}
dst[0] = 1.0;
for (int i = 1; i <= 2; i++)
{
dst[i] = 0.0;
for (int y = 1; y <= i; y++)
dst[i] += mtx[i][y] * dst[i - y];
}
}
static void MergeFinishRecord(const tvec src, tvec dst)
{
tvec tmp;
double val = src[0];
dst[0] = 1.0;
for (int i = 1; i <= 2; i++)
{
double v2 = 0.0;
for (int y = 1; y < i; y++)
v2 += dst[y] * src[i - y];
if (val > 0.0)
dst[i] = -(v2 + src[i]) / val;
else
dst[i] = 0.0;
tmp[i] = dst[i];
for (int y = 1; y < i; y++)
dst[y] += dst[i] * dst[i - y];
val *= 1.0 - (dst[i] * dst[i]);
}
FinishRecord(tmp, dst);
}
static double ContrastVectors(const tvec source1, const tvec source2)
{
double val = (source2[2] * source2[1] + -source2[1]) / (1.0 - source2[2] * source2[2]);
double val1 = (source1[0] * source1[0]) + (source1[1] * source1[1]) + (source1[2] * source1[2]);
double val2 = (source1[0] * source1[1]) + (source1[1] * source1[2]);
double val3 = source1[0] * source1[2];
return val1 + (2.0 * val * val2) + (2.0 * (-source2[1] * val + -source2[2]) * val3);
}
static void FilterRecords(tvec vecBest[8], int exp, tvec records[], int recordCount)
{
tvec bufferList[8];
int buffer1[8];
tvec buffer2;
int index;
double value, tempVal = 0;
for (int x = 0; x < 2; x++)
{
for (int y = 0; y < exp; y++)
{
buffer1[y] = 0;
for (int i = 0; i <= 2; i++)
bufferList[y][i] = 0.0;
}
for (int z = 0; z < recordCount; z++)
{
index = 0;
value = 1.0e30;
for (int i = 0; i < exp; i++)
{
tempVal = ContrastVectors(vecBest[i], records[z]);
if (tempVal < value)
{
value = tempVal;
index = i;
}
}
buffer1[index]++;
MatrixFilter(records[z], buffer2);
for (int i = 0; i <= 2; i++)
bufferList[index][i] += buffer2[i];
}
for (int i = 0; i < exp; i++)
if (buffer1[i] > 0)
for (int y = 0; y <= 2; y++)
bufferList[i][y] /= buffer1[i];
for (int i = 0; i < exp; i++)
MergeFinishRecord(bufferList[i], vecBest[i]);
}
}
void correlateCoefs(int16_t* source, uint32_t samples, int16_t* coefsOut)
{
uint32_t numFrames = (samples + 13) / 14;
uint32_t frameSamples;
short* blockBuffer = (short*)calloc(sizeof(short), 0x3800);
short pcmHistBuffer[2][14] = { 0 };
tvec vec1;
tvec vec2;
tvec mtx[3];
int vecIdxs[3];
tvec* records = (tvec*)calloc(sizeof(tvec), numFrames * 2);
int recordCount = 0;
tvec vecBest[8];
// Iterate though 1024-block frames
for (uint32_t x = samples; x > 0;)
{
if (x > 0x3800) // Full 1024-block frame
{
frameSamples = 0x3800;
x -= 0x3800;
}
else // Partial frame
{
// Zero lingering block samples
frameSamples = x;
for (int z = 0; z < 14 && z + frameSamples < 0x3800; z++)
blockBuffer[frameSamples + z] = 0;
x = 0;
}
// Copy (potentially non-frame-aligned PCM samples into aligned buffer)
memcpy(blockBuffer, source, frameSamples * sizeof(short));
source += frameSamples;
for (uint32_t i = 0; i < frameSamples;)
{
for (int z = 0; z < 14; z++)
pcmHistBuffer[0][z] = pcmHistBuffer[1][z];
for (int z = 0; z < 14; z++)
pcmHistBuffer[1][z] = blockBuffer[i++];
InnerProductMerge(vec1, pcmHistBuffer[1]);
if (fabs(vec1[0]) > 10.0)
{
OuterProductMerge(mtx, pcmHistBuffer[1]);
if (!AnalyzeRanges(mtx, vecIdxs))
{
BidirectionalFilter(mtx, vecIdxs, vec1);
if (!QuadraticMerge(vec1))
{
FinishRecord(vec1, records[recordCount]);
recordCount++;
}
}
}
}
}
vec1[0] = 1.0;
vec1[1] = 0.0;
vec1[2] = 0.0;
for (int z = 0; z < recordCount; z++)
{
MatrixFilter(records[z], vecBest[0]);
for (int y = 1; y <= 2; y++)
vec1[y] += vecBest[0][y];
}
for (int y = 1; y <= 2; y++)
vec1[y] /= recordCount;
MergeFinishRecord(vec1, vecBest[0]);
int exp = 1;
for (int w = 0; w < 3;)
{
vec2[0] = 0.0;
vec2[1] = -1.0;
vec2[2] = 0.0;
for (int i = 0; i < exp; i++)
for (int y = 0; y <= 2; y++)
vecBest[exp + i][y] = (0.01 * vec2[y]) + vecBest[i][y];
++w;
exp = 1 << w;
FilterRecords(vecBest, exp, records, recordCount);
}
// Write output
for (int z = 0; z < 8; z++)
{
double d;
d = -vecBest[z][1] * 2048.0;
if (d > 0.0)
coefsOut[z * 2] = (d > 32767.0) ? (short)32767 : (short)lround(d);
else
coefsOut[z * 2] = (d < -32768.0) ? (short)-32768 : (short)lround(d);
d = -vecBest[z][2] * 2048.0;
if (d > 0.0)
coefsOut[z * 2 + 1] = (d > 32767.0) ? (short)32767 : (short)lround(d);
else
coefsOut[z * 2 + 1] = (d < -32768.0) ? (short)-32768 : (short)lround(d);
}
// Free memory
free(records);
free(blockBuffer);
}
// Make sure source includes the yn values (16 samples total)
void DSPEncodeFrame(short pcmInOut[16], int sampleCount, unsigned char adpcmOut[8], const short coefsIn[8][2])
{
int inSamples[8][16];
int outSamples[8][14];
int bestIndex = 0;
int scale[8];
double distAccum[8];
// Iterate through each coef set, finding the set with the smallest error
for (int i = 0; i < 8; i++)
{
int v1, v2, v3;
int distance, index;
// Set yn values
inSamples[i][0] = pcmInOut[0];
inSamples[i][1] = pcmInOut[1];
// Round and clamp samples for this coef set
distance = 0;
for (int s = 0; s < sampleCount; s++)
{
// Multiply previous samples by coefs
inSamples[i][s + 2] = v1 = ((pcmInOut[s] * coefsIn[i][1]) + (pcmInOut[s + 1] * coefsIn[i][0])) / 2048;
// Subtract from current sample
v2 = pcmInOut[s + 2] - v1;
// Clamp
v3 = (v2 >= 32767) ? 32767 : (v2 <= -32768) ? -32768 : v2;
// Compare distance
if (abs(v3) > abs(distance))
distance = v3;
}
// Set initial scale
for (scale[i] = 0; (scale[i] <= 12) && ((distance > 7) || (distance < -8)); scale[i]++, distance /= 2)
{
}
scale[i] = (scale[i] <= 1) ? -1 : scale[i] - 2;
do
{
scale[i]++;
distAccum[i] = 0;
index = 0;
for (int s = 0; s < sampleCount; s++)
{
// Multiply previous
v1 = ((inSamples[i][s] * coefsIn[i][1]) + (inSamples[i][s + 1] * coefsIn[i][0]));
// Evaluate from real sample
v2 = (pcmInOut[s + 2] << 11) - v1;
// Round to nearest sample
v3 = (v2 > 0) ? (int)((double)v2 / (1 << scale[i]) / 2048 + 0.4999999f) : (int)((double)v2 / (1 << scale[i]) / 2048 - 0.4999999f);
// Clamp sample and set index
if (v3 < -8)
{
if (index < (v3 = -8 - v3))
index = v3;
v3 = -8;
}
else if (v3 > 7)
{
if (index < (v3 -= 7))
index = v3;
v3 = 7;
}
// Store result
outSamples[i][s] = v3;
// Round and expand
v1 = (v1 + ((v3 * (1 << scale[i])) << 11) + 1024) >> 11;
// Clamp and store
inSamples[i][s + 2] = v2 = (v1 >= 32767) ? 32767 : (v1 <= -32768) ? -32768 : v1;
// Accumulate distance
v3 = pcmInOut[s + 2] - v2;
distAccum[i] += v3 * (double)v3;
}
for (int x = index + 8; x > 256; x >>= 1)
if (++scale[i] >= 12)
scale[i] = 11;
} while ((scale[i] < 12) && (index > 1));
}
double min = DBL_MAX;
for (int i = 0; i < 8; i++)
{
if (distAccum[i] < min)
{
min = distAccum[i];
bestIndex = i;
}
}
// Write converted samples
for (int s = 0; s < sampleCount; s++)
pcmInOut[s + 2] = (short)inSamples[bestIndex][s + 2];
// Write ps
adpcmOut[0] = (char)((bestIndex << 4) | (scale[bestIndex] & 0xF));
// Zero remaining samples
for (int s = sampleCount; s < 14; s++)
outSamples[bestIndex][s] = 0;
// Write output samples
for (int y = 0; y < 7; y++)
adpcmOut[y + 1] = (char)((outSamples[bestIndex][y * 2] << 4) | (outSamples[bestIndex][y * 2 + 1] & 0xF));
}
void encodeFrame(int16_t* src, uint8_t* dst, int16_t* coefs, uint8_t one)
{
DSPEncodeFrame(src, 14, dst, (const short(*)[2])&coefs[0]);
}

View File

@@ -0,0 +1,69 @@
/* (c) 2017 Alex Barney (MIT) */
#include "dsptool.h"
uint32_t getBytesForAdpcmBuffer(uint32_t samples)
{
uint32_t frames = samples / SAMPLES_PER_FRAME;
if (samples % SAMPLES_PER_FRAME)
frames++;
return frames * BYTES_PER_FRAME;
}
uint32_t getBytesForAdpcmInfo(void)
{
return sizeof(ADPCMINFO);
}
uint32_t getBytesForAdpcmSamples(uint32_t samples)
{
uint32_t extraBytes = 0;
uint32_t frames = samples / SAMPLES_PER_FRAME;
uint32_t extraSamples = samples % SAMPLES_PER_FRAME;
if (extraSamples)
extraBytes = (extraSamples / 2) + (extraSamples % 2) + 1;
return BYTES_PER_FRAME * frames + extraBytes;
}
uint32_t getBytesForPcmBuffer(uint32_t samples)
{
uint32_t frames = samples / SAMPLES_PER_FRAME;
if (samples % SAMPLES_PER_FRAME)
frames++;
return frames * SAMPLES_PER_FRAME * sizeof(int16_t);
}
uint32_t getBytesForPcmSamples(uint32_t samples)
{
return samples * sizeof(int16_t);
}
uint32_t getNibbleAddress(uint32_t samples)
{
uint32_t frames = samples / SAMPLES_PER_FRAME;
uint32_t extraSamples = samples - (frames * SAMPLES_PER_FRAME);
return NIBBLES_PER_FRAME * frames + extraSamples + 2;
}
uint32_t getNibblesForNSamples(uint32_t samples)
{
uint32_t frames = samples / SAMPLES_PER_FRAME;
uint32_t extraSamples = samples - (frames * SAMPLES_PER_FRAME);
uint32_t extraNibbles = extraSamples == 0 ? 0 : extraSamples + 2;
return NIBBLES_PER_FRAME * frames + extraNibbles;
}
uint32_t getSampleForAdpcmNibble(uint32_t nibble)
{
uint32_t frames = nibble / NIBBLES_PER_FRAME;
uint32_t extraNibbles = nibble - (frames * NIBBLES_PER_FRAME);
uint32_t samples = SAMPLES_PER_FRAME * frames;
return samples + extraNibbles - 2;
}

152
feropm.py Executable file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
# Name: feropm.py
# Copyright: © 2020 a dinosaur
# Homepage: https://github.com/ScrelliCopter/VGM-Tools
# License: Zlib (https://opensource.org/licenses/Zlib)
# Description: Script to convert csMD presets to VOPM files.
# Based on documentation provided by MovieMovies1.
from argparse import ArgumentParser
from xml.dom import minidom
from pathlib import Path
from typing import TextIO
class Preset:
class Operator:
tl = 0
ml = 0
dt = 0
ar = 0
d1 = 0
sl = 0
d2 = 0
r = 0
am_mask = 0
ssg_eg = 0
kr = 0
velo = 0
ks_lvl = 0
def __init__(self):
self.name = ""
self.alg = 0
self.fb = 0
self.lfo_vib = 0
self.lfo_trem = 0
self.op = [self.Operator() for i in range(4)]
def parse_fermatap(path: Path) -> Preset:
xdoc = minidom.parse(str(path))
xpreset = xdoc.getElementsByTagName("Preset")
xtarget = xpreset[0].getElementsByTagName("Target")
xparams = xtarget[0].getElementsByTagName("Param")
patch = Preset()
clamp = lambda x, a, b: x if x < b else b if x > a else a
invert = lambda x, a, b: b - clamp(x, a, b)
def parseop(op, id, x):
if id == 0:
patch.op[op].tl = invert(x, 0, 127)
elif id == 1:
patch.op[op].ml = clamp(x, 0, 15)
elif id == 2:
x = clamp(x, -3, 3)
patch.op[op].dt = x if x >= 0 else 4 - x
elif id == 3:
patch.op[op].ar = invert(x, 0, 31)
elif id == 4:
patch.op[op].d1 = invert(x, 0, 31)
elif id == 5:
patch.op[op].sl = invert(x, 0, 15)
elif id == 6:
patch.op[op].d2 = invert(x, 0, 31)
elif id == 7:
patch.op[op].r = invert(x, 0, 15)
elif id == 8:
patch.op[op].am_mask = clamp(x, 0, 1)
elif id == 9:
patch.op[op].ssg_eg = clamp(x, 0, 8)
elif id == 10:
patch.op[op].kr = clamp(x, 0, 3)
elif id == 11:
patch.op[op].velo = clamp(x, 0, 3)
elif id == 12:
patch.op[op].ks_lvl = clamp(x, 0, 99)
for i in xparams:
id = int(i.attributes["id"].value)
x = int(i.attributes["value"].value)
if 0 <= id <= 15:
parseop(0, id, x)
elif id <= 31:
parseop(1, id - 16, x)
elif id <= 47:
parseop(2, id - 32, x)
elif id <= 63:
parseop(3, id - 48, x)
elif id == 64:
patch.alg = clamp(x, 0, 7)
elif id == 65:
patch.fb = clamp(x, 0, 7)
elif id == 66:
patch.lfo_vib = clamp(x, 0, 7)
elif id == 67:
patch.lfo_trem = clamp(x, 0, 3)
elif id <= 71:
pass
else:
print("unrecognised parameter id {}".format(id))
return patch
def save_vopm(path: Path, patch: Preset):
fmtnum = lambda n: " " + f"{n}".rjust(3, " ")
def writech(file: TextIO):
file.write("CH:")
file.write(fmtnum(64))
file.write(fmtnum(patch.fb))
file.write(fmtnum(patch.alg))
file.write(fmtnum(patch.lfo_trem))
file.write(fmtnum(patch.lfo_vib))
file.write(fmtnum(120))
file.write(fmtnum(0))
file.write("\n")
def writeop(file: TextIO, label: str, op: Preset.Operator):
file.write(label)
for i in [op.ar, op.d1, op.d2, op.r, op.sl, op.tl, op.kr, op.ml, op.dt, 0, op.am_mask]:
file.write(fmtnum(i))
file.write("\n")
with path.open("w", encoding="utf-8") as file:
file.write(f"@:0 {patch.name}\n")
file.write("LFO: 0 0 0 0 0\n")
writech(file)
writeop(file, "M1:", patch.op[0])
writeop(file, "C1:", patch.op[1])
writeop(file, "M2:", patch.op[2])
writeop(file, "C2:", patch.op[3])
def main():
p = ArgumentParser(description="Convert fermatap-formatted csMD presets to VOPM (.opm) files.")
p.add_argument("infile", type=Path, help="Path to input csMD (.fermatap) file")
p.add_argument("--output", "-o", type=Path, required=False, help="Name of output VOPM (.opm) file")
args = p.parse_args()
patch = parse_fermatap(args.infile)
patch.name = args.output.name.rstrip(".opm") if args.output is not None else args.infile.name.rstrip(".fermatap")
outpath = args.output if args.output is not None else args.infile.parent.joinpath(patch.name + ".opm")
save_vopm(outpath, patch)
if __name__ == "__main__":
main()

View File

@@ -1,109 +0,0 @@
/* neoadpcmextract.cpp
Copyright (C) 2017 Nicholas Curtis
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
#include <cstdint>
void DecodeSample ( std::ifstream& a_file, std::vector<uint8_t>& a_out )
{
// Set up output vector.
uint32_t sampLen = 0;
a_file.read ( (char*)&sampLen, sizeof(uint32_t) );
if ( sampLen < sizeof(uint64_t) )
{
return;
}
sampLen -= sizeof(uint64_t);
a_out.clear ();
a_out.resize ( sampLen );
// Ignore 8 bytes.
uint64_t dummy;
a_file.read ( (char*)&dummy, sizeof(uint64_t) );
// Read adpcm data.
a_file.read ( (char*)a_out.data (), sampLen );
}
void DumpBytes ( std::string a_path, const std::vector<uint8_t>& a_bytes )
{
std::ofstream fileOut ( a_path, std::ios::binary );
fileOut.write ( (const char*)a_bytes.data (), a_bytes.size () );
fileOut.close ();
}
int main ( int argc, char** argv )
{
if ( argc != 2 )
{
return -1;
}
// Open file.
std::ifstream file ( argv[1], std::ios::binary );
if ( !file.is_open () )
{
return -1;
}
// Search for pcm headers.
std::vector<uint8_t> smpBytes;
int smpA = 0, smpB = 0;
while ( !file.eof () && !file.fail () )
{
uint8_t byte;
file >> byte;
if ( byte == 0x67 )
{
file >> byte;
if ( byte == 0x66 )
{
file >> byte;
if ( byte == 0x82 )
{
std::cout << "ADPCM-A data found at 0x" << std::hex << file.tellg () << std::endl;
DecodeSample ( file, smpBytes );
std::stringstream path;
path << std::hex << "smpa_" << (smpA++) << ".pcm";
DumpBytes ( path.str (), smpBytes );
}
else
if ( byte == 0x83 )
{
std::cout << "ADPCM-B data found at 0x" << std::hex << file.tellg () << std::endl;
DecodeSample ( file, smpBytes );
std::stringstream path;
path << std::hex << "smpb_" << (smpB++) << ".pcm";
DumpBytes ( path.str (), smpBytes );
}
}
}
}
file.close ();
return 0;
}

24
neotools/CMakeLists.txt Normal file
View File

@@ -0,0 +1,24 @@
find_package(ZLIB)
option(USE_ZLIB "Link Zlib for VGZ support" ${ZLIB_FOUND})
if (USE_ZLIB AND NOT ZLIB_FOUND)
message(FATAL_ERROR "USE_ZLIB specified but Zlib was not found")
endif()
add_executable(adpcm adpcm.h libadpcma.c adpcm.c)
set_property(TARGET adpcm PROPERTY C_STANDARD 99)
target_compile_options(adpcm PRIVATE ${WARNINGS})
target_link_libraries(adpcm Common::wave $<$<C_COMPILER_ID:Clang,GNU>:m>)
add_executable(adpcmb adpcmb.h libadpcmb.c adpcmb.c)
set_property(TARGET adpcmb PROPERTY C_STANDARD 99)
target_compile_options(adpcmb PRIVATE ${WARNINGS})
target_link_libraries(adpcmb Common::wave)
add_executable(neoadpcmextract
libadpcma.c adpcm.h
libadpcmb.c adpcmb.h
neoadpcmextract.c)
set_property(TARGET neoadpcmextract PROPERTY C_STANDARD 99)
target_compile_definitions(neoadpcmextract PRIVATE $<$<BOOL:${USE_ZLIB}>:USE_ZLIB=1>)
target_compile_options(neoadpcmextract PRIVATE ${WARNINGS})
target_link_libraries(neoadpcmextract $<$<BOOL:${USE_ZLIB}>:ZLIB::ZLIB> Common::wave)

46
neotools/README.md Normal file
View File

@@ -0,0 +1,46 @@
Neo-Geo VGM tools
=================
A hodge-podge of tools for working on NG VGM files,
these files are provided in the hope that they may be useful to someone.
Included tools (sources included).
- **adpcm**:
ADPCM Type-A to WAV converter.
- **adpcmb**:
ADPCM Type-B encoding tool that also does decoding to WAV.
- **neoadpcmextract**:
Scans a .vgm and dumps all ADPCM type A&B data to raw .pcm files.
- **autoextract**:
Convenience shell/batch script that uses the above tools to dump all samples to WAVs.
Building
--------
Linux:
```shell script
make -f adpcm.Makefile
make -f adpcmb.Makefile
make -f neoadpcmextract.Makefile
```
Windows:
- The updated Makefiles haven't been tested on Windows yet.
- Working Windows binaries are available from the [Releases](https://github.com/ScrelliCopter/VGM-Tools/releases) tab.
How to use
----------
Linux:
- Run `./autoextract.sh` from this directory with a path to your .vgm or .vgz,
a directory for the song will be created containing wav samples.
Windows:
- You will need gzip.exe (provided with the aforementioned release).
- Copy your .vgm or .vgz into this directory, drag it onto autoextract.bat,
the script will create a directory of the same name and dump all the converted samples there.
That's all there is to it.
TODO
----
- Unify all tools into one & obsolete the crappy glue scripts.

88
neotools/adpcm.c Normal file
View File

@@ -0,0 +1,88 @@
/* adpcm.c
* original ADPCM to PCM converter v 1.01 By MARTINEZ Fabrice aka SNK of SUPREMACY
*/
#include "adpcm.h"
#include "wave.h"
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE (1024 * 256)
int main(int argc, char* argv[])
{
fprintf(stderr, "**** ADPCM to PCM converter v 1.01\n\n");
if (argc != 3)
{
fprintf(stderr, "USAGE: adpcm <InputFile.pcm> <OutputFile.wav>\n");
return -1;
}
FILE* inFile = fopen(argv[1], "rb");
if (!inFile)
{
fprintf(stderr, "Could not open inputfile %s\n", argv[1]);
return -2;
}
FILE* outFile = fopen(argv[2], "wb");
if (!outFile)
{
fprintf(stderr, "Could not open outputfile %s\n", argv[2]);
return -3;
}
char* InputBuffer = malloc(BUFFER_SIZE);
if (InputBuffer == NULL)
{
fprintf(stderr, "Could not allocate input buffer. (%d bytes)\n", BUFFER_SIZE);
return -4;
}
short* OutputBuffer = malloc(BUFFER_SIZE * 4);
if (OutputBuffer == NULL)
{
fprintf(stderr, "Could not allocate output buffer. (%d bytes)\n", BUFFER_SIZE * 4);
return -5;
}
AdpcmADecoderState decoder;
adpcmAInit(&decoder);
fseek(inFile, 0, SEEK_END);
unsigned int Filelen = ftell(inFile);
fseek(inFile, 0, SEEK_SET);
// Write wave header
waveWrite(&(const WaveSpec)
{
.format = WAVE_FMT_PCM,
.channels = 1,
.rate = 18500,
.bytedepth = 2
},
NULL, Filelen * 4, &waveStreamDefaultCb, outFile);
// Convert ADPCM to PCM and write to wave
int bytesRead;
do
{
bytesRead = fread(InputBuffer, 1, BUFFER_SIZE, inFile);
if (bytesRead > 0)
{
adpcmADecode(&decoder, InputBuffer, OutputBuffer, bytesRead);
fwrite(OutputBuffer, bytesRead * 4, 1, outFile);
}
}
while (bytesRead == BUFFER_SIZE);
free(OutputBuffer);
free(InputBuffer);
fclose(outFile);
fclose(inFile);
fprintf(stderr, "Done...\n");
return 0;
}

14
neotools/adpcm.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef ADPCM_H
#define ADPCM_H
typedef struct AdpcmADecoderState
{
int jediTable[49 * 16];
int cursignal;
int delta;
} AdpcmADecoderState;
void adpcmAInit(AdpcmADecoderState* decoder);
void adpcmADecode(AdpcmADecoderState* decoder, const char* restrict in, short* restrict out, int len);
#endif//ADPCM_H

279
neotools/adpcmb.c Normal file
View File

@@ -0,0 +1,279 @@
/* adpcmb.c - CLI for encoding & decoding YM2610 ADPCM-B files
; Fred/FRONT
;
;Usage 1: ADPCM_Encode -d [-r:reg,clock] Input.bin Output.wav
;Usage 2: ADPCM_Encode -e Input.wav Output.bin
;
; Valley Bell
;----------------------------------------------------------------------------------------------------------------------------*/
#include "adpcmb.h"
#include "wave.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static FORCE_INLINE uint32_t DeltaTReg2SampleRate(uint16_t DeltaN, uint32_t Clock)
{
return (uint32_t)(DeltaN * (Clock / 72.0) / 65536.0 + 0.5);
}
#define BUFFER_SIZE 2048
static int decode(const char* inPath, const char* outPath, uint32_t sampleRate)
{
FILE* inFile = fopen(inPath, "rb");
if (inFile == NULL)
{
printf("Error opening input file!\n");
return 2;
}
fseek(inFile, 0, SEEK_END);
long adpcmSize = ftell(inFile);
fseek(inFile, 0, SEEK_SET);
uint8_t* adpcmData = malloc(BUFFER_SIZE);
int16_t* wavData = malloc(BUFFER_SIZE * 2 * sizeof(int16_t));
FILE* outFile = fopen(outPath, "wb");
if (outFile == NULL)
{
printf("Error opening output file!\n");
free(wavData);
free(adpcmData);
fclose(inFile);
return 3;
}
// Write wave header
waveWrite(&(const WaveSpec)
{
.format = WAVE_FMT_PCM,
.channels = 1,
.rate = sampleRate ? sampleRate : 22050,
.bytedepth = 2
},
NULL, (size_t)adpcmSize * 2 * sizeof(int16_t), &waveStreamDefaultCb, outFile);
printf("Decoding ...");
AdpcmBDecoderState decoder;
adpcmBDecoderInit(&decoder);
size_t read;
do
{
if ((read = fread(adpcmData, 1, BUFFER_SIZE, inFile)) > 0)
{
adpcmBDecode(&decoder, adpcmData, wavData, read);
fwrite(wavData, sizeof(int16_t), read * 2, outFile);
}
}
while (read == BUFFER_SIZE);
printf(" OK\n");
fclose(outFile);
free(wavData);
free(adpcmData);
fclose(inFile);
printf("File written.\n");
return 0;
}
typedef struct waveformat_tag {
uint16_t wFormatTag; /* format type */
uint16_t nChannels; /* number of channels (i.e. mono, stereo...) */
uint32_t nSamplesPerSec; /* sample rate */
uint32_t nAvgBytesPerSec; /* for buffer estimation */
uint16_t nBlockAlign; /* block size of data */
uint16_t wBitsPerSample;
} WAVEFORMAT;
typedef struct
{
char RIFFfcc[4];
uint32_t RIFFLen;
char WAVEfcc[4];
char fmt_fcc[4];
uint32_t fmt_Len;
WAVEFORMAT fmt_Data;
char datafcc[4];
uint32_t dataLen;
} WAVE_FILE;
static int encode(const char* inPath, const char* outPath)
{
FILE* hFile = fopen(inPath, "rb");
if (hFile == NULL)
{
printf("Error opening input file!\n");
return 2;
}
WAVE_FILE WaveFile;
fread(&WaveFile.RIFFfcc, 0x0C, 0x01, hFile);
if (memcmp(WaveFile.RIFFfcc, "RIFF", 4) != 0 || memcmp(WaveFile.WAVEfcc, "WAVE", 4) != 0)
{
fclose(hFile);
printf("This is no wave file!\n");
return 4;
}
unsigned int TempLng = fread(&WaveFile.fmt_fcc, 0x04, 0x01, hFile);
fread(&WaveFile.fmt_Len, 0x04, 0x01, hFile);
while (memcmp(WaveFile.fmt_fcc, "fmt ", 4) != 0)
{
if (!TempLng) // TempLng == 0 -> EOF reached
{
fclose(hFile);
printf("Error in wave file: Can't find format-tag!\n");
return 4;
}
fseek(hFile, WaveFile.fmt_Len, SEEK_CUR);
TempLng = fread(&WaveFile.fmt_fcc, 0x04, 0x01, hFile);
fread(&WaveFile.fmt_Len, 0x04, 0x01, hFile);
};
TempLng = ftell(hFile) + WaveFile.fmt_Len;
fread(&WaveFile.fmt_Data, sizeof(WAVEFORMAT), 0x01, hFile);
fseek(hFile, TempLng, SEEK_SET);
WAVEFORMAT* TempFmt = &WaveFile.fmt_Data;
if (TempFmt->wFormatTag != WAVE_FMT_PCM)
{
fclose(hFile);
printf("Error in wave file: Compressed wave file are not supported!\n");
return 4;
}
if (TempFmt->nChannels != 1)
{
fclose(hFile);
printf("Error in wave file: Unsupported number of channels (%hu)!\n", TempFmt->nChannels);
return 4;
}
if (TempFmt->wBitsPerSample != 16)
{
fclose(hFile);
printf("Error in wave file: Only 16-bit waves are supported! (File uses %hu bit)\n", TempFmt->wBitsPerSample);
return 4;
}
TempLng = fread(&WaveFile.datafcc, 0x04, 0x01, hFile);
fread(&WaveFile.dataLen, 0x04, 0x01, hFile);
while (memcmp(WaveFile.datafcc, "data", 4) != 0)
{
if (!TempLng) // TempLng == 0 -> EOF reached
{
fclose(hFile);
printf("Error in wave file: Can't find data-tag!\n");
return 4;
}
fseek(hFile, WaveFile.dataLen, SEEK_CUR);
TempLng = fread(&WaveFile.datafcc, 0x04, 0x01, hFile);
fread(&WaveFile.dataLen, 0x04, 0x01, hFile);
};
unsigned int WaveSize = WaveFile.dataLen / 2;
int16_t* WaveData = malloc(WaveSize * 2);
fread(WaveData, 0x02, WaveSize, hFile);
fclose(hFile);
unsigned int AdpcmSize = WaveSize / 2;
uint8_t* AdpcmData = malloc(AdpcmSize);
printf("Encoding ...");
AdpcmBEncoderState encoder;
adpcmBEncoderInit(&encoder);
adpcmBEncode(&encoder, WaveData, AdpcmData, WaveSize);
printf(" OK\n");
hFile = fopen(outPath, "wb");
if (hFile == NULL)
{
printf("Error opening output file!\n");
free(AdpcmData);
free(WaveData);
return 3;
}
fwrite(AdpcmData, 0x01, AdpcmSize, hFile);
fclose(hFile);
printf("File written.\n");
free(AdpcmData);
free(WaveData);
return 0;
}
int main(int argc, char* argv[])
{
unsigned int TempLng;
uint16_t DTRegs;
char* TempPnt;
printf("NeoGeo ADPCM-B En-/Decoder\n--------------------------\n");
if (argc < 4)
{
printf("Usage: ADPCM_Encode -method [-option] InputFile OutputFile\n");
printf("-method - En-/Decoding Method:\n");
printf(" -d for decode (bin -> wav)\n");
printf(" -e for encode (wav -> bin)\n");
printf("-option - Options for Sample Rate of decoded Wave:\n");
printf(" -s:rate - Sample Rate in Hz (default: 22050)\n");
printf(" -r:regs[,clock] - DeltaT Register value (use 0x for hex) and chip clock\n");
printf(" (default chip clock: 4 MHz)\n");
printf("\n");
printf("Wave In-/Output is 16-bit, Mono\n");
return 1;
}
if (strcmp(argv[1], "-d") && strcmp(argv[1], "-e"))
{
printf("Wrong option! Use -d or -e!\n");
return 1;
}
int ErrVal = 0;
uint32_t OutSmplRate = 0;
int ArgBase = 2;
if (argv[2][0] == '-' && argv[2][2] == ':')
{
switch (argv[2][1])
{
case 's':
OutSmplRate = strtol(argv[2] + 3, NULL, 0);
break;
case 'r':
DTRegs = (uint16_t)strtoul(argv[2] + 3, &TempPnt, 0);
TempLng = 0;
if (*TempPnt == ',')
{
TempLng = strtoul(TempPnt + 1, NULL, 0);
}
if (!TempLng)
TempLng = 4000000;
OutSmplRate = DeltaTReg2SampleRate(DTRegs, TempLng);
break;
}
if (argc < ++ArgBase + 2)
{
printf("Not enought arguments!\n");
return 1;
}
}
switch (argv[1][1])
{
case 'd':
ErrVal = decode(argv[ArgBase + 0], argv[ArgBase + 1], OutSmplRate);
break;
case 'e':
ErrVal = encode(argv[ArgBase + 0], argv[ArgBase + 1]);
break;
}
return ErrVal;
}

25
neotools/adpcmb.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef ADPCMB_H
#define ADPCMB_H
#include <stdint.h>
#include <stdbool.h>
typedef struct AdpcmBEncoderState
{
bool flag;
long xn, stepSize;
uint8_t adpcmPack;
} AdpcmBEncoderState;
void adpcmBEncoderInit(AdpcmBEncoderState* encoder);
void adpcmBEncode(AdpcmBEncoderState* encoder, const int16_t* restrict in, uint8_t* restrict out, int len);
typedef struct AdpcmBDecoderState
{
long xn, stepSize;
} AdpcmBDecoderState;
void adpcmBDecoderInit(AdpcmBDecoderState* decoder);
void adpcmBDecode(AdpcmBDecoderState* decoder, const uint8_t* restrict in, int16_t* restrict out, int len);
#endif//ADPCMB_H

57
neotools/libadpcma.c Normal file
View File

@@ -0,0 +1,57 @@
/* libadpcma.c (C) 2023 a dinosaur (zlib)
Original ADPCM to PCM converter v 1.01 By MARTINEZ Fabrice aka SNK of SUPREMACY */
#include "adpcm.h"
#include "util.h"
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#define ADPCMA_VOLUME_RATE 1
#define ADPCMA_DECODE_RANGE 1024
#define ADPCMA_DECODE_MIN (-(ADPCMA_DECODE_RANGE * ADPCMA_VOLUME_RATE))
#define ADPCMA_DECODE_MAX ((ADPCMA_DECODE_RANGE * ADPCMA_VOLUME_RATE) - 1)
void adpcmAInit(AdpcmADecoderState* decoder)
{
for (int step = 0; step <= 48; step++)
{
int stepval = floor(16.0 * pow(11.0 / 10.0, step) * ADPCMA_VOLUME_RATE);
// Loop over all nibbles and compute the difference
for (int nib = 0; nib < 16; nib++)
{
int value = stepval * ((nib & 0x07) * 2 + 1) / 8;
decoder->jediTable[step * 16 + nib] = (nib & 0x08) ? -value : value;
}
}
decoder->delta = 0;
decoder->cursignal = 0;
}
static const int decodeTableA1[16] =
{
-1 * 16, -1 * 16, -1 * 16, -1 * 16, 2 * 16, 5 * 16, 7 * 16, 9 * 16,
-1 * 16, -1 * 16, -1 * 16, -1 * 16, 2 * 16, 5 * 16, 7 * 16, 9 * 16
};
void adpcmADecode(AdpcmADecoderState* decoder, const char* restrict in, short* restrict out, int len)
{
for (int i = 0; i < len * 2; ++i)
{
int data = (!(i & 0x1) ? ((*in) >> 4) : (*in++)) & 0x0F;
int oldsignal = decoder->cursignal;
decoder->cursignal = CLAMP(decoder->cursignal + decoder->jediTable[data + decoder->delta],
ADPCMA_DECODE_MIN, ADPCMA_DECODE_MAX);
decoder->delta = CLAMP(decoder->delta + decodeTableA1[data], 0 * 16, 48 * 16);
if (abs(oldsignal - decoder->cursignal) > 2500)
{
fprintf(stderr, "WARNING: Suspicious signal evolution %06x,%06x pos:%06x delta:%06x\n",
oldsignal, decoder->cursignal, i % len, decoder->delta);
fprintf(stderr, "data:%02x dx:%08x\n",
data, decoder->jediTable[data + decoder->delta]);
}
*(out++) = (decoder->cursignal & 0xffff) * 32;
}
}

92
neotools/libadpcmb.c Normal file
View File

@@ -0,0 +1,92 @@
/* libadpcmb.c (C) 2023 a dinosaur (zlib)
** YM2610 ADPCM-B Codec **
PCM to ADPCM-B & ADPCM-B to PCM converters for NEO-GEO System
ADPCM-B - 1 channel 1.8-55.5 KHz, 16 MB Sample ROM size,
256 B min size of sample, 16 MB max, compatable with YM2608
http://www.raregame.ru/file/15/YM2610.pdf YM2610 DATASHEET
*/
#include "adpcmb.h"
#include "util.h"
#include <stdlib.h>
static const long stepSizeTable[16] =
{
57, 57, 57, 57, 77, 102, 128, 153,
57, 57, 57, 57, 77, 102, 128, 153
};
void adpcmBEncoderInit(AdpcmBEncoderState* encoder)
{
encoder->xn = 0;
encoder->stepSize = 127;
encoder->flag = false;
encoder->adpcmPack = 0;
}
void adpcmBEncode(AdpcmBEncoderState* encoder, const int16_t* restrict in, uint8_t* restrict out, int len)
{
for (int lpc = 0; lpc < len; ++lpc)
{
long dn = (*in++) - encoder->xn;
long i = (labs(dn) << 16) / (encoder->stepSize << 14);
i = MIN(i, 7);
uint8_t adpcm = i;
i = (adpcm * 2 + 1) * encoder->stepSize / 8;
if (dn < 0)
{
adpcm |= 0x8;
encoder->xn -= i;
}
else
{
encoder->xn += i;
}
encoder->stepSize = (stepSizeTable[adpcm] * encoder->stepSize) / 64;
encoder->stepSize = CLAMP(encoder->stepSize, 127, 24576);
if (!encoder->flag)
{
encoder->adpcmPack = adpcm << 4;
encoder->flag = true;
}
else
{
(*out++) = encoder->adpcmPack |= adpcm;
encoder->flag = false;
}
}
}
void adpcmBDecoderInit(AdpcmBDecoderState* decoder)
{
decoder->xn = 0;
decoder->stepSize = 127;
}
void adpcmBDecode(AdpcmBDecoderState* decoder, const uint8_t* restrict in, int16_t* restrict out, int len)
{
for (long lpc = 0; lpc < len * 2; ++lpc)
{
uint8_t adpcm = (!(lpc & 0x1) ? (*in) >> 4 : (*in++)) & 0xF;
long i = ((adpcm & 7) * 2 + 1) * decoder->stepSize / 8;
if (adpcm & 8)
decoder->xn -= i;
else
decoder->xn += i;
decoder->xn = CLAMP(decoder->xn, -32768, 32767);
decoder->stepSize = decoder->stepSize * stepSizeTable[adpcm] / 64;
decoder->stepSize = CLAMP(decoder->stepSize, 127, 24576);
(*out++) = (int16_t)decoder->xn;
}
}

184
neotools/neoadpcmextract.c Normal file
View File

@@ -0,0 +1,184 @@
/* neoadpcmextract.c (C) 2017, 2019, 2020, 2023 a dinosaur (zlib) */
#include "neoadpcmextract.h"
#include "adpcm.h"
#include "adpcmb.h"
#include "wave.h"
#include "endian.h"
#include "util.h"
#include <stdlib.h>
#include <stdio.h>
static uint32_t read32le(nfile* fin)
{
uint32_t tmp = 0;
nread(&tmp, sizeof(uint32_t), 1, fin);
return SWAP_LE32(tmp);
}
bool bufferResize(Buffer* buf, size_t size)
{
if (!buf)
return false;
buf->size = size;
if (!buf->data || buf->reserved < size)
{
free(buf->data);
buf->reserved = size;
buf->data = malloc(size);
if (!buf->data)
return false;
}
return true;
}
int vgmReadSample(nfile* restrict fin, Buffer* restrict buf)
{
uint32_t sampLen = read32le(fin); // Get sample data length
if (sampLen <= 8)
return 1;
sampLen -= 8;
if (!bufferResize(buf, sampLen)) // Resize buffer if needed
return false;
nseek(fin, 8, SEEK_CUR); // Ignore 8 bytes
nread(buf->data, 1, sampLen, fin); // Read adpcm data
return 0;
}
int vgmScanSample(nfile* file)
{
// Scan for pcm headers
while (1)
{
if (neof(file) || nerror(file))
return 0;
if (ngetc(file) != 0x67 || ngetc(file) != 0x66) // Match data block
continue;
switch (ngetc(file))
{
case 0x82: return 'A'; // 67 66 82 - ADPCM-A
case 0x83: return 'B'; // 67 66 83 - ADPCM-B
default: return 0;
}
}
}
#define DECODE_BUFFER_SIZE 0x4000
int writeAdpcmA(int id, const Buffer* enc, Buffer* pcm)
{
char name[32];
snprintf(name, sizeof(name), "smpa_%02x.wav", id);
FILE* fout = fopen(name, "wb");
if (!fout)
return 1;
// Write wave header
const uint32_t decodedSize = enc->size * 2 * sizeof(short);
waveWrite(&(const WaveSpec)
{
.format = WAVE_FMT_PCM,
.channels = 1,
.rate = 18500,
.bytedepth = 2
},
NULL, decodedSize, &waveStreamDefaultCb, fout);
bufferResize(pcm, DECODE_BUFFER_SIZE * 2 * sizeof(short));
AdpcmADecoderState decoder;
adpcmAInit(&decoder);
size_t decoded = 0;
do
{
const size_t blockSize = MIN(enc->size - decoded, DECODE_BUFFER_SIZE);
adpcmADecode(&decoder, &((const char*)enc->data)[decoded], (short*)pcm->data, blockSize);
fwrite(pcm->data, sizeof(short), blockSize * 2, fout);
decoded += DECODE_BUFFER_SIZE;
}
while (decoded < enc->size);
fclose(fout);
fprintf(stderr, "Wrote \"%s\"\n", name);
return 0;
}
int writeAdpcmB(int id, const Buffer* enc, Buffer* pcm)
{
char name[32];
snprintf(name, sizeof(name), "smpb_%02x.wav", id);
FILE* fout = fopen(name, "wb");
if (!fout)
return 1;
// Write wave header
const uint32_t decodedSize = enc->size * 2 * sizeof(short);
waveWrite(&(const WaveSpec)
{
.format = WAVE_FMT_PCM,
.channels = 1,
.rate = 22050,
.bytedepth = 2
},
NULL, decodedSize, &waveStreamDefaultCb, fout);
bufferResize(pcm, DECODE_BUFFER_SIZE * 2 * sizeof(short));
AdpcmBDecoderState decoder;
adpcmBDecoderInit(&decoder);
size_t decoded = 0;
do
{
const size_t blockSize = MIN(enc->size - decoded, DECODE_BUFFER_SIZE);
adpcmBDecode(&decoder, &((const uint8_t*)enc->data)[decoded], (int16_t*)pcm->data, blockSize);
fwrite(pcm->data, sizeof(int16_t), blockSize * 2, fout);
decoded += DECODE_BUFFER_SIZE;
}
while (decoded < enc->size);
fclose(fout);
fprintf(stderr, "Wrote \"%s\"\n", name);
return 0;
}
int main(int argc, char** argv)
{
if (argc != 2)
return 1;
nfile* file = nopen(argv[1], "rb"); // Open file
if (!file) return 1;
#if !USE_ZLIB
if (ngetc(file) == 0x1F && ngetc(file) == 0x8B)
{
printf("I'm a little gzip short and stout\n");
return 2;
}
nseek(file, 0, SEEK_SET);
#endif
Buffer rawbuf = BUFFER_CLEAR(), decbuf = BUFFER_CLEAR();
int smpaCount = 0, smpbCount = 0;
// Find ADCPM samples
int scanType;
while ((scanType = vgmScanSample(file)))
{
fprintf(stderr, "ADPCM-%c data found at 0x%08lX\n", scanType, ntell(file));
if (vgmReadSample(file, &rawbuf) || rawbuf.size == 0)
continue;
if (scanType == 'A')
writeAdpcmA(smpaCount++, &rawbuf, &decbuf);
else if (scanType == 'B')
writeAdpcmB(smpbCount++, &rawbuf, &decbuf);
}
free(rawbuf.data);
nclose(file);
return 0;
}

View File

@@ -0,0 +1,39 @@
#ifndef NEOADPCMEXTRACT_H
#define NEOADPCMEXTRACT_H
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#if USE_ZLIB
#include <zlib.h>
typedef struct gzFile_s nfile;
# define nopen gzopen
# define nclose gzclose
# define nread gzfread
# define ngetc gzgetc
# define nseek gzseek
# define ntell gztell
# define neof gzeof
static inline int nerror(gzFile file) { int err; gzerror(file, &err); return err; }
#else
typedef FILE nfile;
# define nopen fopen
# define nclose fclose
# define nread fread
# define ngetc fgetc
# define nseek fseek
# define ntell ftell
# define neof feof
# define nerror ferror
#endif
typedef struct { void* data; size_t size, reserved; } Buffer;
#define BUFFER_CLEAR() { NULL, 0, 0 }
bool bufferResize(Buffer* buf, size_t size);
int vgmReadSample(nfile* restrict fin, Buffer* restrict buf);
int vgmScanSample(nfile* file);
#endif//NEOADPCMEXTRACT_H

78
sinharmonicswt.py Normal file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# Name: sinharmonicswt.py
# Copyright: © 2023 a dinosaur
# Homepage: https://github.com/ScrelliCopter/VGM-Tools
# License: Zlib (https://opensource.org/licenses/Zlib)
# Description: Generate Serum format wavetables of the harmonic series
# for a handful of common FM waveforms, intended for improving
# the workflow of creating FM sounds in Vital.
import math
from pathlib import Path
from typing import NamedTuple
from common.wavewriter import WaveFile, WavePcmFormatChunk, WaveDataChunk
from common.waveserum import WaveSerumCommentChunk, SerumWavetableInterpolation
def write_wavetable(name: str, generator,
mode: SerumWavetableInterpolation = SerumWavetableInterpolation.NONE,
num: int = 64, size: int = 2048):
def sweep_table() -> bytes:
for i in range(num - 1):
yield b"".join(generator(size, i + 1))
with open(name, "wb") as f:
WaveFile(WavePcmFormatChunk(1, 44100, 16), [
WaveSerumCommentChunk(size, mode),
WaveDataChunk(b"".join(sweep_table()))
]).write(f)
def main():
clip = lambda x, a, b: max(a, min(b, x))
def clamp2short(a: float) -> int: return clip(int(a * 0x7FFF), -0x8000, 0x7FFF)
def sinetable16(size: int, harmonic: int):
# Generate a simple sine wave
for i in range(size):
sample = clamp2short(math.sin(i / size * math.tau * harmonic))
yield sample.to_bytes(2, byteorder="little", signed=True)
#TODO: probably make bandlimited versions of the nonlinear waves
def hsinetable16(size: int, harmonic: int):
# Generate one half of a sine wave with the negative pole hard clipped off
for i in range(size):
sample = clamp2short(max(0.0, math.sin(i / size * math.tau * harmonic)))
yield sample.to_bytes(2, byteorder="little", signed=True)
#TODO: probably make bandlimited versions of the nonlinear waves
def asinetable16(size: int, harmonic: int):
# Generate a sine wave with the negative pole mirrored positively
for i in range(size):
sample = clamp2short(math.fabs(math.sin(i / size * math.pi * harmonic)))
yield sample.to_bytes(2, byteorder="little", signed=True)
outfolder = Path("FM Harmonics")
outfolder.mkdir(exist_ok=True)
# Build queue of files to generate
GenItem = NamedTuple("GenItem", generator=any, steps=int, mode=SerumWavetableInterpolation, name=str)
genqueue: list[GenItem] = list()
# All waveform types with 64 harmonic steps in stepped and linear versions
for mode in [("", SerumWavetableInterpolation.NONE), (" (XFade)", SerumWavetableInterpolation.LINEAR_XFADE)]:
for generator in [("Sine", sinetable16), ("Half Sine", hsinetable16), ("Abs Sine", asinetable16)]:
genqueue.append(GenItem(generator[1], 64, mode[1], f"{generator[0]} Harmonics{mode[0]}"))
# Shorter linear versions of hsine and asine
for steps in [8, 16, 32]:
spec = SerumWavetableInterpolation.LINEAR_XFADE
for generator in [("Half Sine", hsinetable16), ("Abs Sine", asinetable16)]:
genqueue.append(GenItem(generator[1], steps, spec, f"{generator[0]} (XFade {steps})"))
# Generate & write wavetables
for i in genqueue:
write_wavetable(str(outfolder.joinpath(f"{i.name}.wav")), i.generator, i.mode, i.steps)
if __name__ == "__main__":
main()

178
spctools/ripsamples.py Executable file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
# ripsamples.py -- a python script for mass extracting samples from SPC files.
# (C) 2018, 2023 a dinosaur (zlib)
import os
import subprocess
import pathlib
import struct
import hashlib
from typing import BinaryIO
from io import BytesIO
import sys
sys.path.append("..")
from common.wavewriter import WaveFile, WavePcmFormatChunk, WaveDataChunk
from common.wavesampler import WaveSamplerChunk, WaveSamplerLoop
# Directory constants
SPCDIR = "./spc"
ITDIR = "./it"
SMPDIR = "./sample"
# External programs used by this script
SPC2IT = "spc2it/spc2it"
class Sample:
length = 0
loopBeg = 0
loopEnd = 0
rate = 0
data: bytes = None
def writesmp(smp: Sample, path: str):
with open(path, "wb") as wav:
# Make sure sample rate is nonzero
#TODO: figure out why this even happens...
if smp.rate == 0:
smp.rate = 32000
#print(path + " may be corrupted")
fmtChunk = WavePcmFormatChunk( # Audio format (uncompressed)
1, # Channel count (mono)
smp.rate, # Samplerate
16) # Bits per sample (16 bit)
dataChunk = WaveDataChunk(smp.data)
loopChunk = None
if smp.loopEnd > smp.loopBeg:
loopChunk = WaveSamplerChunk(loops=[WaveSamplerLoop(
start=smp.loopBeg, # Loop start
end=smp.loopEnd)]) # Loop end
WaveFile(fmtChunk,
[dataChunk] if loopChunk is None else [loopChunk, dataChunk]
).write(wav)
def readsmp(f: BinaryIO, ofs: int, idx: int):
# List of assumptions made:
# - Samples are 16 bit
# - Samples are mono
# - Samples are signed
# - No compression
# - No sustain loop
# - Loops are forwards
# - Global volume is ignored
f.seek(ofs)
if f.read(4) != b"IMPS": return None
# Skip fname to flags & read
f.seek(ofs + 0x12)
flags = int.from_bytes(f.read(1), byteorder="little", signed=False)
# Read flag values
if not flags & 0b00000001: return None # Check sample data bit
loopBit = True if flags & 0b00010000 else False
smp = Sample()
# Read the rest of the header
f.seek(ofs + 0x30)
smp.length = int.from_bytes(f.read(4), byteorder="little", signed=False)
if loopBit:
smp.loopBeg = int.from_bytes(f.read(4), byteorder="little", signed=False)
smp.loopEnd = int.from_bytes(f.read(4), byteorder="little", signed=False)
else:
f.seek(8, 1) # Skip over
smp.loopBeg = 0
smp.loopEnd = 0
smp.rate = int.from_bytes(f.read(4), byteorder="little", signed=False)
f.seek(8, 1) # Skip over sustain shit
# Read sample data
dataOfs = int.from_bytes(f.read(4), byteorder="little", signed=False)
smp.data = f.read(smp.length * 2)
# Compute hash of data
#FIXME: This actually generates a butt ton of collisions...
# there's got to be a better way!
h = hashlib.md5(struct.pack("<pII", smp.data, smp.loopBeg, smp.loopEnd))
smp.hash = h.hexdigest()
return smp
def readit(path: str, outpath: str):
with open(path, "r+b") as f:
# Don't bother scanning non IT files
if f.read(4) != b"IMPM": return
#print("Song name: " + f.read(26).decode('utf-8'))
# Need order list size and num instruments to know how far to skip
f.seek(0x20)
ordNum = int.from_bytes(f.read(2), byteorder="little", signed=False)
insNum = int.from_bytes(f.read(2), byteorder="little", signed=False)
smpNum = int.from_bytes(f.read(2), byteorder="little", signed=False)
patNum = int.from_bytes(f.read(2), byteorder="little", signed=False)
# Cus spc2it has a tendency to produce corrupted files...
if ordNum > 1024: return
if smpNum > 4000: return
if insNum > 256: return
if patNum > 256: return
smpOfsTable = 0xC0 + ordNum + insNum * 4
for i in range(0, smpNum):
f.seek(smpOfsTable + i * 4)
smpOfs = int.from_bytes(f.read(4), byteorder="little", signed=False)
smp = readsmp(f, smpOfs, i + 1)
if smp != None:
outwav = os.path.join(outpath, smp.hash + ".wav")
if not os.path.isfile(outwav):
pathlib.Path(outpath).mkdir(parents=True, exist_ok=True)
writesmp(smp, outwav)
def scanit(srcPath: str, dstPath: str):
for directory, subdirectories, files in os.walk(srcPath):
for file in files:
if file.endswith(".it"):
path = os.path.join(directory, file)
outpath = dstPath + path[len(srcPath):-len(file)]
readit(path, outpath)
def scanspc(srcPath: str, dstPath: str):
for directory, subdirectories, files in os.walk(srcPath):
# Create output dir for each game
for sub in subdirectories:
path = os.path.join(dstPath, sub)
pathlib.Path(path).mkdir(parents=True, exist_ok=True)
# Convert spc files
for file in files:
if file.endswith(".spc"):
# Don't convert files that have already been converted
itpath = os.path.join(dstPath + directory[len(srcPath):], file[:-3] + "it")
if not os.path.isfile(itpath):
path = os.path.join(directory, file)
subprocess.call([SPC2IT, path])
path = path[:-3] + "it"
if os.path.isfile(path):
os.rename(path, itpath)
# Actual main stuff
if __name__ == "__main__":
scanspc(SPCDIR, ITDIR)
scanit(ITDIR, SMPDIR)

View File

@@ -0,0 +1,10 @@
---
Language: Cpp
BasedOnStyle: LLVM
AllowShortFunctionsOnASingleLine: Inline
BreakConstructorInitializersBeforeComma: true
ColumnLimit: 120
IndentWidth: 4
TabWidth: 4
UseTab: ForIndentation
BreakBeforeBraces: Allman

8
spctools/spc2it/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
obj/
build/
spc2it
*.exe
*.spc
*.it
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 2.8)
project(spc2it)
set(spc2it_sources
emu.c
it.c
main.c
sound.c
spc700.c
emu.h
it.h
sneese_spc.h
sound.h
spc2ittypes.h)
add_executable(spc2it ${spc2it_sources})
target_link_libraries(spc2it m)

42
spctools/spc2it/Makefile Normal file
View File

@@ -0,0 +1,42 @@
TARGET := spc2it
SOURCE := emu.c it.c main.c sound.c spc700.c
CFLAGS ?= -O2 -pipe
BUILD_CFLAGS := $(CFLAGS)
BUILD_LDFLAGS := $(CFLAGS) $(LDFLAGS) -lm
PREFIX := /usr/local
OBJDIR := obj
OBJECTS := $(patsubst %.c,$(OBJDIR)/%.o,$(SOURCE))
DEPENDS := $(OBJECTS:%.o=%.d)
default: all $(TARGET)
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(BUILD_CFLAGS) -MMD -c $< -o $@
$(OBJDIR):
mkdir -p $@
$(TARGET): $(OBJECTS)
$(CC) $(BUILD_LDFLAGS) $^ -o $@
-include $(DEPENDS)
all: $(TARGET)
.PHONY: install
install: $(TARGET)
mkdir -p $(DESTDIR)$(PREFIX)/bin
cp $< $(DESTDIR)$(PREFIX)/bin/$(TARGET)
mkdir -p $(DESTDIR)$(PREFIX)/share/man/man1/
cp spc2it.1 $(DESTDIR)$(PREFIX)/share/man/man1/
.PHONY: uninstall
uninstall:
rm -f $(DESTDIR)$(PREFIX)/bin/$(TARGET)
rm -f $(DESTDIR)$(PREFIX)/share/man/man1/spc2it.1
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJECTS) $(DEPENDS)

49
spctools/spc2it/README.md Executable file
View File

@@ -0,0 +1,49 @@
SPC2IT
======
Convert SPC files to IT (Impulse Tracker) files.
## Compiling
With GNU make:
```bash
make -j$(nproc)
```
With CMake:
```bash
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
```
## Installing
```bash
# Install to /usr/local by default
sudo make install
# Install to /usr instead
sudo make install PREFIX=/usr
```
## Running
Run `spc2it` (or `./sp2it` locally) with no arguments to see the syntax.
```
SPC2IT - converts SPC700 sound files to the Impulse Tracker format
Usage: spc2it [options] <filename>
Where <filename> is any .spc or .sp# file
Options: -t x Specify a time limit in seconds [60 default]
-d xxxxxxxx Voices to disable (1-8) [none default]
-r xxx Specify IT rows per pattern [200 default]
```
## More information
Cloned from: https://github.com/uyjulian/spc2it
For more information, read the documentation in `doc/`.
Also, see https://www.romhacking.net/forum/index.php?topic=10164.0

View File

@@ -0,0 +1,160 @@
This license is a free software license, compatible with the GNU General
Public License (GPL). It is the minimal set of changes needed to correct
the vagueness of the Original Artistic License.
Text'ified by Randy McDowell (stainless* *at* *users.sourceforge.net).
SNEeSe is copyright (c) 1998-2006, Charles Bilyue'.
Portions copyright (c) 1998-2003, Brad Martin.
Portions copyright (c) 2003-2004, Daniel Horchner.
Portions copyright (c) 2004-2005, Nach. ( http://nsrt.edgeemu.com/ )
Unzip Technology, copyright (c) 1998 Gilles Vollant.
zlib Technology ( www.gzip.org/zlib/ ), Copyright (c) 1995-2003,
Jean-loup Gailly ( jloup* *at* *gzip.org ) and Mark Adler
( madler* *at* *alumni.caltech.edu ).
JMA Technology, copyright (c) 2004-2005 NSRT Team. ( http://nsrt.edgeemu.com/ )
LZMA Technology, copyright (c) 2001-4 Igor Pavlov. ( http://www.7-zip.org )
Portions copyright (c) 2002 Andrea Mazzoleni. ( http://advancemame.sf.net )
- The Clarified Artistic License -
i. Preamble
The intent of this document is to state the conditions under which a
Package may be copied, such that the Copyright Holder maintains some
semblance of artistic control over the development of the package, while
giving the users of the package the right to use and distribute the
Package in a more-or-less customary fashion, plus the right to make
reasonable modifications.
ii. Definitions:
"Package" refers to the collection of files distributed by the Copyright
Holder, and derivatives of that collection of files created through
textual modification.
"Standard Version" refers to such a Package if it has not been modified,
or has been modified in accordance with the wishes of the Copyright
Holder as specified below.
"Copyright Holder" is whoever is named in the copyright or copyrights
for the package.
"You" is you, if you're thinking about copying or distributing this
Package.
"Distribution fee" is a fee you charge for providing a copy of this
Package to another party.
"Freely Available" means that no fee is charged for the right to use the
item, though there may be fees involved in handling the item. It also
means that recipients of the item may redistribute it under the same
conditions they received it.
iii. Context
1. You may make and give away verbatim copies of the source form of the
Standard Version of this Package without restriction, provided that
you duplicate all of the original copyright notices and associated
disclaimers.
2. You may apply bug fixes, portability fixes and other modifications
derived from the Public Domain, or those made Freely Available, or
from the Copyright Holder. A Package modified in such a way shall
still be considered the Standard Version.
3. You may otherwise modify your copy of this Package in any way,
provided that you insert a prominent notice in each changed file
stating how and when you changed that file, and provided that you do
at least ONE of the following:
a) place your modifications in the Public Domain or otherwise make
them Freely Available, such as by posting said modifications to
Usenet or an equivalent medium, or placing the modifications on a
major network archive site allowing unrestricted access to them,
or by allowing the Copyright Holder to include your modifications
in the Standard Version of the Package.
b) use the modified Package only within your corporation or
organization.
c) rename any non-standard executables so the names do not conflict
with standard executables, which must also be provided, and
provide a separate manual page for each non-standard executable
that clearly documents how it differs from the Standard Version.
d) make other distribution arrangements with the Copyright Holder.
e) permit and encourge anyone who receives a copy of the modified
Package permission to make your modifications Freely Available in
some specific way.
4. You may distribute the programs of this Package in object code or
executable form, provided that you do at least ONE of the following:
a) distribute a Standard Version of the executables and library
files, together with instructions (in the manual page or
equivalent) on where to get the Standard Version.
b) accompany the distribution with the machine-readable source of the
Package with your modifications.
c) give non-standard executables non-standard names, and clearly
document the differences in manual pages (or equivalent), together
with instructions on where to get the Standard Version.
d) make other distribution arrangements with the Copyright Holder.
e) offer the machine-readable source of the Package, with your
modifications, by mail order.
5. You may charge a distribution fee for any distribution of this
Package. If you offer support for this Package, you may charge any
fee you choose for that support. You may not charge a license fee
for the right to use this Package itself. You may distribute this
Package in aggregate with other (possibly commercial and possibly
nonfree) programs as part of a larger (possibly commercial and
possibly nonfree) software distribution, and charge license fees for
other parts of that software distribution, provided that you do not
advertise this Package as a product of your own. If the Package
includes an interpreter, You may embed this Package's interpreter
within an executable of yours (by linking); this shall be construed
as a mere form of aggregation, provided that the complete Standard
Version of the interpreter is so embedded.
6. The scripts and library files supplied as input to or produced as
output from the programs of this Package do not automatically fall
under the copyright of this Package, but belong to whoever generated
them, and may be sold commercially, and may be aggregated with this
Package. If such scripts or library files are aggregated with this
Package via the so-called "undump" or "unexec" methods of producing a
binary executable image, then distribution of such an image shall
neither be construed as a distribution of this Package nor shall it
fall under the restrictions of Paragraphs 3 and 4, provided that you
do not represent such an executable image as a Standard Version of
this Package.
7. C subroutines (or comparably compiled subroutines in other languages)
supplied by you and linked into this Package in order to emulate
subroutines and variables of the language defined by this Package
shall not be considered part of this Package, but are the equivalent
of input as in Paragraph 6, provided these subroutines do not change
the language in any way that would cause it to fail the regression
tests for the language.
8. Aggregation of the Standard Version of the Package with a commercial
distribution is always permitted provided that the use of this
Package is embedded; that is, when no overt attempt is made to make
this Package's interfaces visible to the end user of the commercial
distribution. Such use shall not be construed as a distribution of
this Package.
9. The name of the Copyright Holder may not be used to endorse or
promote products derived from this software without specific prior
written permission.
10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.

296
spctools/spc2it/doc/OPENSPC.TXT Executable file
View File

@@ -0,0 +1,296 @@
This is a modified version of OpenSPC 0.301, improved by Dwedit,
with support of fractional update rates to generate better IT files.
See http://www.romhacking.net/forum/index.php?topic=10164.0
/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
OpenSPC ver.301
/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
Intro
-----
OpenSPC is an SPC Player created using a modified version of SNEeSe's ASM SPC
core. The rest was written in C using DJGPP. You can find new versions and
a FAQ for OpenSPC at:
http://home.gvi.net/~martin
OpenSPC was created by Butcha, and has been contributed to by Cyber Warrior X,
Crono, Kadar, and Cait Sith 2.
++++++++++++++++
Table of Contents
--------------------------------------------
1.) What's New
2.) Disclaimer and License
3.) System Requirements
4.) Current Progress
5.) Future Progress
6.) Configuration File
7.) IT Dumping
8.) Credits/Special Thanks
+++++++++++++
1.) What's New
--------------------------------------------
v0.301 - Relatively small update
* Added ID support for SPC files (thanks to Cait Sith 2). Displays info for
SPC files that contain it, in both the lite version and the full version,
non-gui mode.
* Changed "SPC->IT conversion" name in IT files to the ID song name if
specified in SPC file.
* Updated to SNEeSe version .36 SPC code, including all optimizations and
bugfixes that were present in it.
v0.300 - Another big (and overdue) update
* Prevented notes that are out of IT's range from writing the invalid data -
prevents IT corruption, but probably still won't sound quite right.
* Checked for invalid loop points supplied by an SPC file (wouldn't think
you'd *need* to check... %#!@$).
* Updated display screen with new pitch when sliding.
* Fixed rare bug which allowed volumes with opposite signs in both channels
to cancel each other out when in mono mode.
* Corrected bug where if a voice was keyed off at the same time the sample
ended, the voice would not get turned off. Prevents the big long strings of
note cut commands sometimes seen in the IT files.
* Decided to scrap my sound drivers in favor of Allegro's. This means both
the Ensoniq SoundScape and ESS AudioDrive should now be supported in addition
to all SoundBlaster types. Note only the sound driver, not the mixer, has
been replaced.
* As a side effect of this, the onscreen display updates slower now, because
Allegro requires much more sound data to be mixed and delivered at once.
This is much more noticable with frequencies less than 22050 Hz.
* Config file rewritten to reflect changes in sound driver. Should still be
compatible if you have any old frontends, but I'm not sure.
* Corrected a bug in ADSR/GAIN which could cause the envelope height to wrap
around infinitely. (Jurassic Park)
* Implemented main volume registers
* Changed to the newest version of SNEeSe SPC core. TCALL (and therefore
Terranigma) now works! Other optimizations and fixes are also present.
* Changed to SNEeSe's SPC timer code. Timers are now cycle-exact rather than
calculated once every update.
* Reimplemented ADSR/GAIN in the style of the timer code, which means it is
now cycle-exact. Any song that changed envelope states while monitoring ENVX
should now work much better.
* Doubled output volume both in the mixer and in IT dumping. This can
potentially cause clipping distortion while mixing and maxed-out volumes in
IT files if the volume ever gets too high, but it seems SNES games rarely
use high volume which is why it sounded so quiet in the first place.
* Fixed up the display for text mode. Colors and layout should be identical
to the graphics mode, especially if used with an 80x50 screen. Use setgui=2
in the config file to activate.
* Implemented a command line switch equivalent to the setgui parameter in the
config file. (-g x)
* Fixed up the colors on the display and allowed the colors to be changed in
the config file.
* Did a little cleaning and optimization of the source code.
v0.200 - Lots new in this version
* Allowed IT dumping with other than 200 rows/pattern (change in CFG file)
* Fixed bug with volume and IT dumping (played too loud and would
sometimes result in undesired volume column effects being applied)
* Fixed segmentation fault bug when a negative volume applied to a channel
* Fixed a bug with GAIN linear decrease mode
* Fixed file loader bug when filename preceded with "..\" <Doh, stupid me>
* Fixed file loader bug when loading files with extension .SP1, .SP2, etc.
* Increased number of SPC cycles to execute, while simultaneously
implementing system to pause emulation if SPC idle. Should prevent games
like Starfox from slowing down and speeding up, and *might* even save some
CPU power in other games
* Got rid of the "SPC Sample" for samples without lengths. (Thanks to Crono)
* Added .IT pattern compression. (Crono)
* Reduced memory requirements when *NOT* logging a .IT file (Crono)
* Now uses "specific" mixing method for each style of output. (Crono)
* Old Soundblaster drivers tweaked, should sound better and can now go up to
45454 Hz. May introduce some incompatibility, let me know. (Crono)
* Soundblaster Pro drivers tweaked, hopefully sound better. (Crono)
* Moved actual emulation stuff from main.c to emu.c. main.c now only
contains main() and other graphics-related stuff.
* Added support for Ensoniq Soundscape, SoundFX and MediaFX cards (thanks to
Kadar)
* Added support for non-standard IRQs. Let me know if you still have
problems getting it to play on your SB-compatible sound card.
* Found and eliminated something WinAmp wasn't liking about the way unlooped
samples were saved.
* Created 'lite' version! Now capture SPC's to either IT or WAV files
direct to disk at maximum speed rather than having to listen to them.
Uses command line options to specify parameters. (Replaced 'main.c' with
'lite.c' and removed sound drivers in project file)
* Added many of the same command line options to the full version. If no
options are specified it will default to what's in the OPENSPC.CFG file.
* Added a preset time limit and current time indicator to both versions.
* Replaced the SNEeSe SPC core with a newer version; had hoped this would fix
some SPC bugs, but no luck there. Oh well, hopefully its faster or
something.
* Broke down and did some SPC debugging myself; managed to get Prince of
Persia, WWF, Doom, FF2 (Fabul Castle), SOM2 (Boss Theme), and Simpsons:
Bart's Nightmare working a lot better.
Details: - all SBC to memory were subtracting in the wrong order
- CMP YA, dp didn't complement the carry flag
- membit addressing mode operand bits were mapped incorrectly
* Corrected some bugs in GAIN Increase and Decrease modes and re-enabled
ENVX reporting; made WWF, Doom, and Simpsons sound even better.
* Added a reverse stereo option for those of you experiencing swapped sound
channels with an SBPro compatible.
v0.100 - Initial release
+++++++++++++++++++++++++
2.) Disclaimer and License
--------------------------------------------
This program is still considered to be in beta status. Neither Butcha nor any
of the other contributors are responsible for any undesirable effects of using
this program. Also, none of the authors are responsible for the possesion or
use of any copyrighted material taken from copyrighted ROMs using this
program.
Please do not redistribute this program without its included documentation.
Also, do not package this program with SPC or IT files from copyrighted games.
++++++++++++++++++++++
3.) System Requirements
--------------------------------------------
The light version should have very few requirements. Probably a 386 with an
FPU could run it, but I can't say for sure. Memory shouldn't be a problem as
long as you have some available swap space. The following are some tentative
guidelines for the full version:
Minimum Recommended System:
- 486/100 processor
- 8MB RAM
- Sound Blaster or compatible card
Strongly Recommended System :
- Pentium processor (P133 or higher)
- The more RAM the merrier (especially with Windows 9x)
- VGA card (for graphical display)
- Sound Blaster 16 or 100% compatible
+++++++++++++++++++
4.) Current Progress
--------------------------------------------
The following are implemented in the sound engine:
- 16bit digital stereo sound
- SPC700 Sound CPU (likely still bugs to be found)
- FULL ADSR and GAIN volume effects (now cycle-exact!)
- Song capture in the IT format
- Sound recording to WAV format (lite version only)
The following are missing in the sound engine:
- ECHO
- FIR filter
- Noise
- Modulation
The following are some known bugs:
- If the pitch changes too fast while a note is on, an IT file will not be
able to reproduce it fast enough, resulting in a subtle "rounding" effect.
(i.e. FF3 Atma Weapon)
- A few games' music doesn't like being played with a fixed update-per-second
method, resulting in unpredictable effects
- Any game that changes the sample location or data while playing will sound
strange (Doom, Cannon Fodder). No easy work around this, because ITs always
expect the same sample to stay the same.
- Dragon Quest seems to have some severe bugs, no idea why
++++++++++++++++++
5.) Future Progress
--------------------------------------------
Future progress is completely indefinite at this point, due to my currently
attending college at UMR. I haven't had a lot of free time here, and that
which I do have I will most likely be devoted to other things. As far as
major features missing from the program go, there isn't a whole lot more
I can add, because it wouldn't transfer into an IT file anyway (echo, filter).
Noise and modulation might be possibilities, I'd have to look more into it. I
would like to have a much better display, but I keep saying that and never get
around to working on it. Also, there are the above bugs to fix. I would
however like to rewrite the IT code to save more data temporarily into a
proprietary format, and then convert this data to an IT file. This would make
it easier for other people to add support for other filetypes. It could also
make the IT code itself more efficient in compressing the resulting file. I
have no idea when or if I'll get to this, however. We'll see.
+++++++++++++++++++++
6.) Configuration File (not in lite version)
--------------------------------------------
Any documentation you need for the configuration file should be found in
comments inside the OPENSPC.CFG file itself. If you've let some frontend
overwrite it and you can't find any comments, you'll have to restore from the
original ZIP. I recommend backing up the config before using any frontends,
if the frontend doesn't do so itself. Do NOT delete this file, OpenSPC will
NOT regenerate it.
+++++++++++++
7.) IT Dumping
--------------------------------------------
IT dumping is done via 16 channels(8 channels each with independent left and
right volume control). When you switch on IT dumping, everything you hear in
the player is saved into the IT file. This means the IT file starts on the
first note of the SPC and ends when you hit a key to end recording. Upon
pressing a key, a note cut command is issued across all 16 channels, along
with a command to loop back to the beginning of the song. This makes it easy
to record simple looping songs-simply hit a key just before the first note of
the song plays again, and it should sound pretty decent. However, many songs
have an "intro" that they play first, and then go into another section and
loop. To make these loop correctly, you will have to manually edit the IT
yourself. I will describe how to do it using Impulse Tracker; it is up to you
to figure it out if you use a different one, but it shouldn't be much
different.
- First of all, switch on IT dumping and play the song. Stop recording a
second or two after you hear it loop.
- Open up the resulting IT file in Impulse Tracker.
- Find the first note of the section that loops. If it is at the beginning of
a pattern, you are incredibly lucky. If not, go to the next pattern and see
where it begins with relation to the music(for example, say the pattern
begins 5 rows before channel 2 plays a C#5). Also, remember which pattern
number it is.
- Go find the end of the song. Locate the first note of the looped section,
then look forward until you find the spot you remembered(5 rows before
channel 2 plays C#5, in our example). Hit ALT-DEL to delete everything in
this pattern after that spot. When the pattern is clear after that spot,
put the cursor on the effects column of any channel and enter in effect 'B'
followed by the number of the pattern you found in the last step(in hex).
- Once you've completed this, you'll probably want to delete any extra
patterns after this one, along with their respective orders.
- Play it. Sound right? Save it. If not you either found the wrong spot or
entered in the wrong pattern number.
It may sound a little complicated or awkward, but its really not that bad when
you've done it a few times. I did it 16 times to create the examples on my
webpage. If you'd like you can open them up for an example.
+++++++++++++++++++++++++
8.) Credits/Special Thanks
--------------------------------------------
Thanks go to:
- Savoury SnaX and Charles Bilyu for the SPC code used in this project.
- Shawn Hargreaves (and others) for the Allegro library
- Citizen X for letting CWX see the WinSPC source for ideas
- Gary Henderson for helping CWX with SPC Dumping and with Terranigma
- The rest of the Snes9x team for their hard work and the release of the
Snes9x source code, although this program contains no Snes9x-derived code.
- zsKnight and _Demo_ for an awesome emulator and for inventing SPC files
(even though I still think I came up with the idea for playing a sound save-
state first :)
- TheGun and SailorStarFighter for some early betatesting (hi guys :)
--Butcha

View File

@@ -0,0 +1,180 @@
OpenSPC v.300Linux (based on Lite/Direct To Disk version)
Linux port and general code tweaking by Chris Timmerberg (ctimmerb@usa.net)
Hi! Here's the long-awaited port of OpenSPC to linux. I say linux, primarily
because that's all I'm able to test it with (at least presently). If your
platform is relatively portable and has a /dev/dsp it may very well work
there as well. It may even work with code that pretends to be /dev/dsp, such
as I've seen for Os/2. This is completely speculation, I've never tried it.
Even in the lack of /dev/dsp, you should still be able to compile it almost
everywhere there's a gcc or other standard compiler. This will give you full
wav/it dumping facilities, as well as the ability to pipe stdout into
another player app which handles wav-like data. I've found splay to work
fine. Again, I havent tested others. Feel free to do so yourself.
The tty files were copied from another project and as such contain alot of
extra stuff #ifdef'ed which linux doesnt use. I can't say whether or not
these other code blocks work on their respective platforms, sorry.
The binary I send along was built on my 486 running Stampede. It uses no
extra libraries beyond Glibc 2.1. Everyone should have this by now. If you
dont, feel free to compile your own copy (of the OpenSPC, or of glibc).
PGCC 1.1.3 was used. The makefile includes -O6 -mpentium optimization levels
by default. This seems to generate the best quality code for most people's
systems. And no, mpentium DOESNT seem to harm a 486 any. My 486 is doing
just lovely playing back the spc's I've tried. I always use at least these
options when compiling everything (occasionally a couple extras in
addition).
The overall behavior of this port is somewhat different from the original.
Assuming one has enough cpu power (it shouldnt be hard to, my 486 did it)
you can dump to several destinations at the same time. This includes wav
file, IT file, stdout, and soundcard. The program code sets all output
variables to 0. Options you specify for -w -i -p and -l then turn on any of
the 4. If none was specified it turns on wav. Each enabled option is then
checked for success and if necessary turned off. If no output options remain
it finally gives up, and exits.
Data is written directly to /dev/dsp. The original code made no attempt to
buffer writes, and I didn't make any modifications in that area. As far as I
can tell, /dev/dsp and/or the soundcard itself have at least a small buffer.
This is probably not sufficient to prevent breakups if you're doing
something cpu-intense. A buffer may be added eventually...
Several command line options were added:
-l enables the audio card output code. As many sound cards do not possess an
exact frequency match, the audio output is by default tweaked to match what
your soundcard reports it was set to. This does not SEEM TO affect it
output, but does affect wav. If your card's status report was in error or
your goal was to dump an exact frequency wav you may disable the adjustment
with the -a option.
-p enables piping support. This data is identical to that which is fed to a
wav file or /dev/dsp. You may send this directly to an audio player of your
choice.
The -P option to accept input from stdin is wishful thinking for a next
version. It'd be nice to be able to zcat, bzcat, or unzip -c compressed spc
file(s) into this player, and beats trying to write support code for
handling different compression methods.
Aside from everything mentioned above, the base code remains for the most
part, unchanged. I include the two text files from the original dos version,
renamed to openspc.dos.txt and source.dos.txt.
The next version may also include as well as the mentioned earlier buffering
system and input pipes, the ability to batch play multiple files, general
code cleanup and optimization, and maybe some other sound file formats...
Candidates for other formats include gym, cym, nsf, and anything else that
isnt supported by other linux players. Within reason, and as long as my 486
handles it reliably. :)
If you want a netscape plugin, write it yourself. I dont do netscape
plugins. (Netscape is piggish enough on a 486 without having MORE overhead,
thank you very much!!) Meanwhile this existing program SHOULD work as a
'helper app' type thingy. No I didnt test that either.
Misc note: This program uses <__math.h> for the pow2 and __log2 functions.
These functions generate inline assembly code, which is both shorter and
faster than calling external library functions from linking with the -lm
option.
Using code from the NORMAL <math.h> header I've determined to
require an alternate sequence of:
#define __USE_EXTERN_INLINES 1
#define __USE_ISOC9X 1
#include <math.h>
This also changes the function names required, to log2f and __pow2
Oddly enough, the code generated by this contains many more instructions of
overhead.
<__math.h> generates a warning about __log1p which I dont even use. This
warning seems to be due to a bad header file, not my code. The warning is
harmless and may be ignored. The other warnings are going to be cleaned up
eventually.
Release notes for version .201Linux:
* Overlooked changing a few instances of 'dump' into 'itout'. This caused IT
dumping to generate incomplete files.
* WAV dumping no longer generates any temp files
* Upgraded from PGCC 1.1.1 to 1.1.3, and threw in -ffast-math in
compilation options.
* Accidentally packed 'source.dos.txt' into the archive twice the first
time around. Only need ONE copy of this :)
Release notes for version .300Linux:
* Downloaded dos version .300 and re-implemented all my changes including
stripping _ symbols in .S files, #including <__math.h> in sound.c, and
tons of stuff in lite.c
* lite.c now features a shutdown function for orderly shutting down.
* lite.c has two extra \r characters by popular demand :)
* Patched spcmain.S to not use self-modifying code in SPC_READ_RAM_ROM
Release notes for version .301Linux:
* Features migrated from the .301 dos version finally - id666 tags and new
spc core. The id tags allow the player to know how long to play a
particular file for.
* The code from the dos version's lite.c was WRONG WRONG WRONG and
generated really silly play times. So I fixed that and changed it to
read the whole id tag header into a 'struct' all in one read.
* The code for -t was also slightly incorrect in the dos version and my
previous release. We now handle all three cases: seconds only with and
without a leading colon, as well as the previous minutes:seconds case.
Also properly handles stuff like 90 or :90 or 0:90 (or even 1:90)...
* spcmain.S from the dos version included more silly self-modifying code in
.301. When will they learn??? .... fixed
* Dos version collided with my usage of the -l parameter. I kept mine, -l
is still linuxout audio method, and a capitalized -T is used to disable
the playback length mentioned in the idtag resulting in unlimited length
playbacks.
* Changed some of that math.h stuff. Gcc 2.95.2 (and pgcc) seem ok with it
the way I have it now, and I didnt feel like playing with it anymore...
* Tried to implement a 'large buffer' option to avoid audio breakups. Ended
up with WORSE breakups. Someone wanna look that over and see what I
missed?
* Meanwhile, I upgraded to a nice Trident 4Dwave card and now am running
with the Alsa drivers (.58a as of this writing). Program works lovely by
default. (At least as long as you've got the oss emulation modules, but
you do, dont you?? :) This card at least gives me the exact sample rates
I ask for, but I'm sure alot of people out there still have cards which
dont so I didnt touch the rate-tweak logic.
If you want a large buffer and to use native alsa playback, try
something like: ./ospc -p CT-1-22-TheHiddenTruth.spc | bag | aplay -m
'bag' is a nice buffering utility that I think came with Timidity's
tools, and aplay is part of the Alsa stuff. Dont bother trying this
approach with certain other play utilities, in particular avoid the one
from sox. You can identify this crappy 'play' program by typing 'play'
and seeing if it says its a frontend to sox. Sox's play just soaks up
tons of cpu and does NOT help (the point of HAVING a buffer is to try
to avoid cpu usage spikes from breaking up the audio, after all!!)
* The dos version did not yet support the id tag's 'fadeout length' value,
and I have not tried to do anything about this yet either. Feel free to
do so for me. :)
I was thinking maybe to use the oss or alsa mixer controls. Note that at
least for my trident, the oss-emulation 'main volume' seems to be using
one of trident's 32-position ones (despite showing in the oss mixers as a
1...100 range), making for rather unsmooth fading if one were to use that.
In fact, all of the trident's volume knobs can get a bit confusing to sort
out. There are at least several availible from alsa mixers with a full 255
position range at least. (I use xamixer. gamix is also nice. Neither is
totally perfect though.)...
* Some extra keypresses - q and escape quit as well as the previous
spacebar. New key functions include p or s to pause. Other activity
such as fast forward and rewind seem awfully difficult to implement. Feel
free to try doing so yourself as always :) Note that quiting or pausing
seem to take place after the buffer is emptied (or at least not QUITE
instantly). Good enough though.
* If you feel compelled to,
make ospcshared
cp -a libspc.so /usr/lib/libspc.so.0
(or any other suitable directory, or just edit your LD_LIBRARY_PATH)
Works here, your milage may vary.
* Changed Makefile to use CFLAGS, and included a nice flag setting for
optimization which I got from who knows where (probably an old posting to
the xmame mailing list).

173
spctools/spc2it/doc/SOURCE.TXT Executable file
View File

@@ -0,0 +1,173 @@
/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
OpenSPC ver.300
Source Code
/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
/ntro
-----
OpenSPC is an SPC Player created using a very modified SNEeSe SPC CPU core.
It was made in C (although the SPC core is in Assembly) using DJGPP. You can
find new versions and a FAQ for OpenSPC at:
http://home.gvi.net/~martin
OpenSPC was created by Butcha, and has been contributed to by Cyber Warrior X,
Crono, and Kadar.
++++++++++++++++
Table of Contents
--------------------------------------------
1.) Disclaimer
2.) License Rights
3.) Programming Conventions
4.) Brief Outline
+++++++++++++
1.) Disclaimer
--------------------------------------------
This program is still considered to be in beta status. Neither Butcha nor any
of the other contributors are responsible for any undesirable effects of using
this program. Also, none of the authors are responsible for the possesion or
use of any copyrighted material taken from copyrighted ROMs using this
program.
+++++++++++++++++
2.) License Rights
--------------------------------------------
- OpenSPC Program and Code - General
----------------------------------
Please do not redistribute either of these without their respective
documentation. Also, do not package them with either SPC or IT files from
copyrighted games.
- OpenSPC Code - Usage Rights
---------------------------
If you would like to modify the source to OpenSPC in some way, you may send
your version of it to the authors, who will choose whether to include your
improvements in future versions. Please do not make a change or improvement
and release your own, separate program. However, independent ports of
OpenSPC to other platforms are welcome, as long as you let the authors know
what you've done. If you would like to use some part of the source in a
program of your own design, you are free to do so, provided the primary
author is aware of what you are doing. Please give the author(s) credit for
any benefits you receive from either the direct use or the examination of this
code.
Blah, legal crap. :( I'm sure none of you out there will try to rip me off...
But please read and follow this section anyway.
+++++++++++++++++++
3.) Programming Conventions
--------------------------------------------
I have tried to make the source as uniform as possible, to make it easier to
read and understand. Also I have tried to comment wherever possible. What
follows is a list of different programming conventions used by me and which
should be used when modifying the source:
- Tab Spacing is 8
- if, for, and other bracketed statements should take the following form:
if(something)
{
//Do some stuff
//Do some more stuff
}
Here are some examples of what NOT to do:
if(something){/*Do some stuff*//*Do some
more stuff*/}
if(something)
{
//Do some stuff
//Do some more stuff
}
if(something){
//Do some stuff
//Do some more stuff
}
If there is only one statement within the brackets, they may be omitted.
However, the statement should be on the following line and tabbed once:
if(something)
//Do only one thing
- Global variables:
- Try to use them as little as possible
- When you do use them, define them in the .c file whose function they most
correspond with. If that .c file is the only one in the project that
needs that variable, define it as static. This goes for functions as
well.
- If a global variable must be seen outside the .c file it is defined in,
include it in the .h file associated with that .c file, and give it a name
that corresponds with that file. For example, any global variable you
will find in the sound.c file is preceded by the letters SND: SNDvoices,
SNDsamples, etc. This also goes for functions.
This is all I can think of right now. For the most part, just observe what
is already there and try to make your code look and work similarly.
++++++++++++++++
4.) Brief Outline
--------------------------------------------
What follows is a rough outline of how the program works:
- Startup: display message, read config file, parse command line
- Init the SPC: allocate memory, call SNEeSe's Reset command, then load in
registers from the input file
- Init the mixer: make up some tables, init some variables
- If in graphics mode, initialize allegro graphics and fader routine
- Init IT dump if capturing to an IT only
- Queue the notes that started when state was saved
- Initialize the Allegro sound stream to receive data from the mixer
- Main loop:
- Check to see whether Allegro wants more data
- If so, keep emulating and mixing until buffer is full
- Otherwise, render the display (if enabled)
- Dump the IT pattern buffers to disk if recording
- Check current time against time limit
- Check to see if we need to toggle voices or exit as a result of keypress
- Shut down stuff
- If dumping to an IT, figure out what the filename would be and save it.
- End program
Below is an explanation of the processing done when Allegro requests data,
found in the function SNDMix() in sound.c:
- Run the SNEeSe SPC
While this is going on, certain actions have been intercepted from SNEeSe
control:
- (new in v300) Timer control is now cycle-exact, and is handled by the
SNEeSe code.
- Reads and writes to the DSP are intercepted so as to return current sound
data and update sound data when something new is written.
- Control returns from the SNEeSe SPC when the number of cycles that should
have gone by in one update have been emulated.
(2048000 cycles / num_updates_per_second)
- Mixer then proceeds to process each of the 8 voices that are currently keyed
on. This includes updating the envelope, and detecting pitch and volume
changes so as to update the IT, as well as mixing the audio. It also
updates a global variable used by the display functions to determine the
current level of each voice and both channels.
- IT code writes the information recorded as having changed since the last
update. It must write it twice, once per each channel, left and right,
because the SPC has two independent volume controls rather than a volume and
a pan. (The display is somewhat faked... using that method to set the pan
doesn't result in a very good recording. Besides, as near as I can tell
volume and pan cannot be set simultaneously in an IT file.)
- Record IT end of row for this completed update. If we have filled a
pattern, use the next buffer. If no other buffer is available, the main
loop must be running too slow. Either way, simply overwrite the pattern we
just recorded since the data has to go SOMEWHERE. It will skip a pattern
though, so if this happens either turn down the sound quality in the config
file, or turn up NUM_PATT_BUFS in it.h.
- End of update, return from interrupt
I'm sure a lot of the mixer, envelope, and IT data stuff could use some
optimization; if you'd like to do it, as long as it doesn't lose any of the
current functionality I'll use it.
--Butcha

210
spctools/spc2it/emu.c Normal file
View File

@@ -0,0 +1,210 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "sound.h"
#include "spc2ittypes.h"
#include "sneese_spc.h"
u8 SPC_DSP[256];
u8 SPCRAM[65536];
u32 Map_Address;
u32 SPC_DSP_DATA;
u32 Map_Byte;
u8 In_CPU;
u32 SPC_CPU_cycle_divisor;
u32 SPC_CPU_cycle_multiplicand;
u32 SPC_CPU_cycles;
u32 SPC_CPU_cycles_mul;
u32 sound_cycle_latch;
void (*SPC_Write_DSP_Hook)(u8); // function pointer
// ID tag stuff
s32 SPCtime;
SPCFileInformation *SPCInfo;
static s32 LoadZState(char *fn)
{
SPCFile *sFile = calloc(1, sizeof(SPCFile));
if (sFile == NULL)
{
printf("Error: could not allocate memory for SPCFile struct\n");
exit(1);
}
FILE *f = fopen(fn, "rb");
if (f == NULL)
{
printf("Error: can't open file\n");
exit(1);
}
fseek(f, 0, SEEK_SET);
fread(sFile, sizeof(SPCFile), 1, f);
fclose(f);
if (strncmp("SNES-SPC700 Sound File Data", sFile->FileTag, 27))
{
printf("Error: invalid file format\n");
exit(1);
}
memcpy(&active_context->PC.w, sFile->Registers.PC, 2);
active_context->YA.b.l = sFile->Registers.A;
active_context->X = sFile->Registers.X;
active_context->YA.b.h = sFile->Registers.Y;
active_context->SP = 0x100 + sFile->Registers.SP;
active_context->PSW = sFile->Registers.PSW;
memcpy(SPCRAM, sFile->RAM, 65536);
memcpy(SPC_DSP, sFile->DSPBuffer, 128);
SPCInfo = calloc(1, sizeof(SPCFileInformation));
if (SPCInfo == NULL)
{
printf("Error: could not allocate memory for SPCInfo struct\n");
exit(1);
}
memcpy(SPCInfo, &sFile->Information, sizeof(SPCFileInformation));
char songLen[4];
strncpy(songLen, SPCInfo->SongLength, 3);
if (songLen[0] >= 0)
SPCtime = atoi(songLen);
else
SPCtime = 0;
if (0 == (SPC_CTRL & 0x80))
active_context->FFC0_Address = SPCRAM;
active_context->timers[0].target = (u8)(SPCRAM[0xFA] - 1) + 1;
active_context->timers[1].target = (u8)(SPCRAM[0xFB] - 1) + 1;
active_context->timers[2].target = (u8)(SPCRAM[0xFC] - 1) + 1;
active_context->timers[0].counter = SPCRAM[0xFD] & 0xF;
active_context->timers[1].counter = SPCRAM[0xFE] & 0xF;
active_context->timers[2].counter = SPCRAM[0xFF] & 0xF;
active_context->PORT_R[0] = SPCRAM[0xF4];
active_context->PORT_R[1] = SPCRAM[0xF5];
active_context->PORT_R[2] = SPCRAM[0xF6];
active_context->PORT_R[3] = SPCRAM[0xF7];
SPC_CPU_cycle_multiplicand = 1;
SPC_CPU_cycle_divisor = 1;
SPC_CPU_cycles_mul = 0;
spc_restore_flags();
free(sFile);
return (0);
}
// PUBLIC (non-static) functions
s32 SPCInit(char *fn)
{
Reset_SPC();
if (LoadZState(fn))
return 1;
return 0;
}
void SPCAddWriteDSPCallback(void (*ToAddCallback)(u8))
{
SPC_Write_DSP_Hook = ToAddCallback;
}
// Called from SPC 700 engine
void DisplaySPC()
{
}
void InvalidSPCOpcode()
{
exit(-1);
}
void SPC_READ_DSP()
{
if ((SPC_DSP_ADDR & 0xf) == 8) // ENVX
SPC_DSP[SPC_DSP_ADDR] = SNDDoEnv(SPC_DSP_ADDR >> 4) >> 24;
}
void SPC_WRITE_DSP()
{
s32 addr_lo = SPC_DSP_ADDR & 0xF, addr_hi = SPC_DSP_ADDR >> 4;
switch (addr_lo)
{
case 3: // Pitch hi
SPC_DSP_DATA &= 0x3F;
break;
case 5: // ADSR1
if ((SPC_DSP[0x4C] & (1 << addr_hi)) && ((SPC_DSP_DATA & 0x80) != (SPC_DSP[SPC_DSP_ADDR] & 0x80)))
{
s32 i;
// First of all, in case anything was already
// going on, finish it up
SNDDoEnv(addr_hi);
if (SPC_DSP_DATA & 0x80)
{
// switch to ADSR--not sure what to do
i = SPC_DSP[(addr_hi << 4) + 6];
SNDvoices[addr_hi].envstate = ATTACK;
SNDvoices[addr_hi].ar = SPC_DSP_DATA & 0xF;
SNDvoices[addr_hi].dr = SPC_DSP_DATA >> 4 & 7;
SNDvoices[addr_hi].sr = i & 0x1f;
SNDvoices[addr_hi].sl = i >> 5;
}
else
{
// switch to a GAIN mode
i = SPC_DSP[(addr_hi << 4) + 7];
if (i & 0x80)
{
SNDvoices[addr_hi].envstate = i >> 5;
SNDvoices[addr_hi].gn = i & 0x1F;
}
else
{
SNDvoices[addr_hi].envx = (i & 0x7F) << 24;
SNDvoices[addr_hi].envstate = DIRECT;
}
}
}
break;
case 6: // ADSR2
// Finish up what was going on
SNDDoEnv(addr_hi);
SNDvoices[addr_hi].sr = SPC_DSP_DATA & 0x1f;
SNDvoices[addr_hi].sl = SPC_DSP_DATA >> 5;
break;
case 7: // GAIN
if ((SPC_DSP[0x4C] & (1 << addr_hi)) && (SPC_DSP_DATA != SPC_DSP[SPC_DSP_ADDR]) &&
!(SPC_DSP[(addr_hi << 4) + 5] & 0x80))
{
if (SPC_DSP_DATA & 0x80)
{
// Finish up what was going on
SNDDoEnv(addr_hi);
SNDvoices[addr_hi].envstate = SPC_DSP_DATA >> 5;
SNDvoices[addr_hi].gn = SPC_DSP_DATA & 0x1F;
}
else
{
SNDvoices[addr_hi].envx = (SPC_DSP_DATA & 0x7F) << 24;
SNDvoices[addr_hi].envstate = DIRECT;
}
}
break;
// These are general registers
case 12: // 0xc
switch (addr_hi)
{
case 4: // Key on
SNDNoteOn(SPC_DSP_DATA);
SPC_DSP_DATA = SPC_DSP[0x4C];
break;
case 5: // Key off
SNDNoteOff(SPC_DSP_DATA);
SPC_DSP_DATA = 0;
break;
}
break;
}
SPC_DSP[SPC_DSP_ADDR] = SPC_DSP_DATA;
}

23
spctools/spc2it/emu.h Normal file
View File

@@ -0,0 +1,23 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
#ifndef EMU_H
#define EMU_H
#include "spc2ittypes.h"
#include "sneese_spc.h"
extern u8 SPC_DSP[256];
extern u8 SPCRAM[65536];
#define TotalCycles (active_context->TotalCycles)
extern u32 SPC_DSP_DATA;
extern s32 SPCtime;
extern SPCFileInformation *SPCInfo;
extern void (*SPC_Write_DSP_Hook)(u8);
#define SPCUpdateRate 100
s32 SPCInit(char *);
void SPCAddWriteDSPCallback(void (*ToAddCallback)(u8));
#endif

546
spctools/spc2it/it.c Normal file
View File

@@ -0,0 +1,546 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "it.h"
#include "sound.h"
#include "emu.h"
static itdata ITdata[8]; // Temp memory for patterns before going to file
static sndsamp *ITSamples[IT_SAMPLE_MAX];
static u8 *ITpattbuf[NUM_PATT_BUFS]; // Where patterns are going to be , before writing to file
static u8 *ITPatterns;
static u32 ITPatternsSize;
static s32 ITpattlen[NUM_PATT_BUFS]; // lengths of each pattern
static s32 ITcurbuf, ITbufpos, ITcurrow; // Pointers into temp pattern buffers
static s32 ITrows; // Number of rows per pattern
static pcm_t p1, p2;
static s32 offset[IT_PATTERN_MAX]; // table of offsets into temp file to each pattern
static s32 curpatt; // which pattern we are on in temp file
static s32 curoffs; // where we are in file
static sndsamp *ITAllocateSample(s32 size)
{
sndsamp *s;
if (((s = calloc(1, sizeof(sndsamp))) == NULL) || ((s->buf = calloc(1, size * 2)) == NULL))
return (NULL);
s->length = size;
s->loopto = -1;
s->freq = 0;
return (s);
}
static s32 ITGetBRRPrediction(u8 filter, pcm_t p1, pcm_t p2)
{
s32 p;
switch (filter)
{
case 0:
return 0;
case 1:
p = p1;
p -= p1 >> 4;
return p;
case 2:
p = p1 << 1;
p += (-(p1 + (p1 << 1))) >> 5;
p -= p2;
p += p2 >> 4;
return p;
case 3:
p = p1 << 1;
p += (-(p1 + (p1 << 2) + (p1 << 3))) >> 6;
p -= p2;
p += (p2 + (p2 << 1)) >> 4;
return p;
}
return 0;
}
static void ITDecodeSampleInternal(s8 s, u8 shift_am, u8 filter)
{
s32 a;
if (shift_am <= 0x0c) // Valid shift count
a = ((s < 8 ? s : s - 16) << shift_am) >> 1;
else
a = s < 8 ? 1 << 11 : (-1) << 11; // Values "invalid" shift counts
a += ITGetBRRPrediction(filter, p1, p2);
if (a > 0x7fff)
a = 0x7fff;
else if (a < -0x8000)
a = -0x8000;
if (a > 0x3fff)
a -= 0x8000;
else if (a < -0x4000)
a += 0x8000;
p2 = p1;
p1 = a;
}
static s32 ITDecodeSample(u16 start, sndsamp **sp)
{
sndsamp *s;
u8 *src;
u16 end;
u32 brrptr, sampptr = 0;
s32 i;
src = &SPCRAM[start];
for (end = 0; !(src[end] & 1); end += 9)
;
i = (end + 9) / 9 * 16;
*sp = s = ITAllocateSample(i);
if (s == NULL)
return 1;
if (src[end] & 2)
s->loopto = 0;
for (brrptr = 0; brrptr <= end;)
{
u8 range = src[brrptr++];
u8 filter = (range & 0x0c) >> 2;
u8 shift_amount = (range >> 4) & 0x0F;
for (i = 0; i < 8; i++, brrptr++)
{
ITDecodeSampleInternal(src[brrptr] >> 4, shift_amount, filter); // Decode high nybble
s->buf[sampptr++] = 2 * p1;
ITDecodeSampleInternal(src[brrptr] & 0x0F, shift_amount, filter); // Decode low nybble
s->buf[sampptr++] = 2 * p1;
}
}
return 0;
}
static void ITUpdateSample(s32 s)
{
s32 i;
struct
{
u16 vptr, lptr;
} *SRCDIR;
i = SPC_DSP[0x5D] << 8; //sample directory table...
SRCDIR = (void *)&SPCRAM[i];
if (ITDecodeSample(SRCDIR[s].vptr, &ITSamples[s]))
return;
if (ITSamples[s]->loopto != -1)
{
ITSamples[s]->loopto = (SRCDIR[s].lptr - SRCDIR[s].vptr) / 9 * 16;
if ((ITSamples[s]->loopto > ITSamples[s]->length) || (ITSamples[s]->loopto < 0))
ITSamples[s]->loopto = -1;
}
}
static s32 ITPitchToNote(s32 pitch, s32 base)
{
f64 tmp;
s32 note;
tmp = log2((f64)pitch / (f64)base) * 12 + 60;
if (tmp > 127)
tmp = 127;
else if (tmp < 0)
tmp = 0;
note = (s32)tmp;
if ((s32)(tmp * 2) != (note * 2))
note++; // correct rounding
return note;
}
static void ITWriteDSPCallback(u8 v)
{
s32 addr_lo = SPC_DSP_ADDR & 0xF;
s32 addr_hi = SPC_DSP_ADDR >> 4;
if (!(addr_lo == 12))
return;
if (!(addr_hi == 4))
return;
s32 i, cursamp, pitch;
v &= 0xFF; // ext
for (i = 0; i < 8; i++)
{
if (v & (1 << i))
{
cursamp = SPC_DSP[4 + (i << 4)]; // Number of current sample
if (cursamp < IT_SAMPLE_MAX) // Only 99 samples supported, sorry
{
if (ITSamples[cursamp] == NULL)
ITUpdateSample(cursamp);
pitch = (s32)(*(u16 *)&SPC_DSP[(i << 4) + 0x02]) * 7.8125; // Ext, Get pitch
if (ITSamples[cursamp]->freq == 0)
ITSamples[cursamp]->freq = pitch;
if ((pitch != 0) && (ITSamples[cursamp] != NULL) &&
(ITSamples[cursamp]->freq != 0)) // Ext, Sample is actually useful?
{
ITdata[i].mask |= IT_MASK_NOTE_SAMPLE_ADJUSTVOLUME; // Update note, sample, and adjust the volume.
ITdata[i].note = ITPitchToNote(pitch, ITSamples[cursamp]->freq); // change pitch to note
ITdata[i].pitch = (s32)(pow(2, ((f64)ITdata[i].note - 60) / 12) *
(f64)ITSamples[cursamp]->freq); // needed for pitch slide detection
ITdata[i].lvol = 0;
ITdata[i].rvol = 0;
// IT code will get sample from DSP buffer
}
}
}
}
}
static void ITSSave(sndsamp *s, FILE *f) // Save sample
{
s32 loopto = -1;
s32 length = 0;
s32 freq = 0;
s32 ofs = ftell(f);
ITFileSample *sHeader = calloc(1, sizeof(ITFileSample));
if (sHeader == NULL)
{
printf("Error: could not allocate memory for ITFileSample struct\n");
exit(1);
}
if (s != NULL)
{
loopto = s->loopto;
length = s->length;
freq = s->freq;
}
else
{
freq = 8363;
loopto = 0;
}
memcpy(sHeader->magic, "IMPS", 4);
if (length)
strcpy(sHeader->fileName, "SPC2ITSAMPLE");
sHeader->GlobalVolume = 64;
sHeader->Flags |= 2; // Bit 1 (16 bit)
if (length)
sHeader->Flags |= 1; // Bit 0 (sample included with header)
sHeader->Volume = 64;
if (length)
strcpy(sHeader->SampleName, "SPC2ITSAMPLE");
sHeader->Convert = 1;
sHeader->DefaultPan = 0;
sHeader->NumberOfSamples = length;
if (loopto != -1)
{
sHeader->Flags |= 16; // Bit 4 (Use loop)
sHeader->LoopBeginning = loopto;
sHeader->LoopEnd = length;
}
sHeader->C5Speed = freq;
if (length)
sHeader->SampleOffset = ofs + sizeof(ITFileSample);
fwrite(sHeader, sizeof(ITFileSample), 1, f);
free(sHeader);
if (length)
fwrite(s->buf, s->length * 2, 1, f); // Write the sample itself... 2x length.
}
static void ITWritePattern(ITPatternInfo *pInfo) {
ITpattbuf[ITcurbuf][ITbufpos++] = pInfo->Channel;
ITpattbuf[ITcurbuf][ITbufpos++] = pInfo->Mask;
if (pInfo->Mask & IT_MASK_NOTE)
ITpattbuf[ITcurbuf][ITbufpos++] = pInfo->Note;
if (pInfo->Mask & IT_MASK_SAMPLE)
ITpattbuf[ITcurbuf][ITbufpos++] = pInfo->Sample;
if (pInfo->Mask & IT_MASK_ADJUSTVOLUME)
ITpattbuf[ITcurbuf][ITbufpos++] = pInfo->Volume;
if (pInfo->Mask & IT_MASK_PITCHSLIDE)
{
ITpattbuf[ITcurbuf][ITbufpos++] = pInfo->Command;
ITpattbuf[ITcurbuf][ITbufpos++] = pInfo->CommandValue;
}
}
s32 ITStart(s32 rows) // Opens up temporary file and inits writing
{
SPCAddWriteDSPCallback(&ITWriteDSPCallback);
s32 i;
ITrows = rows;
for (i = 0; i < NUM_PATT_BUFS; i++)
{
ITpattbuf[i] = calloc(1, Mem64k - 8); //Don't include the 8 byte header
if (ITpattbuf[i] == NULL)
{
printf("Error: could not allocate memory for IT pattern buffer\n");
exit(1);
}
ITpattlen[i] = 0;
}
ITPatterns = calloc(1, Mem64k * IT_PATTERN_MAX);
if (ITPatterns == NULL)
{
printf("Error: could not allocate memory for IT pattern storage\n");
exit(1);
}
ITPatternsSize = 0;
ITcurbuf = 0;
ITbufpos = 0;
ITcurrow = 0;
curoffs = 0;
for (i = 0; i < IT_PATTERN_MAX; i++)
offset[i] = -1; // -1 means unused pattern
curpatt = 0;
for (i = 0; i < 8; i++)
ITdata[i].mask = 0;
return 0;
}
s32 ITUpdate() // Dumps pattern buffers to file
{
u8 *tmpptr;
s32 i;
ITFilePattern *pHeader = calloc(1, sizeof(ITFilePattern));
if (pHeader == NULL)
{
printf("Error: could not allocate memory for ITFilePattern struct\n");
exit(1);
}
for (i = 0; i < ITcurbuf; i++)
{
offset[curpatt] = curoffs;
pHeader->Length = ITpattlen[i];
pHeader->Rows = ITrows;
memcpy(&ITPatterns[ITPatternsSize], pHeader, sizeof(ITFilePattern));
ITPatternsSize += sizeof(ITFilePattern);
memcpy(&ITPatterns[ITPatternsSize], ITpattbuf[i], ITpattlen[i]);
ITPatternsSize += ITpattlen[i];
curoffs += ITpattlen[i] + 8;
if (curpatt < IT_PATTERN_MAX)
curpatt++; // Continue counting if we haven't reached the limit yet
}
free(pHeader);
tmpptr = ITpattbuf[0];
ITpattbuf[0] = ITpattbuf[ITcurbuf];
ITpattbuf[ITcurbuf] = tmpptr;
ITcurbuf = 0;
return 0;
}
s32 ITWrite(char *fn) // Write the final IT file
{
FILE *f;
s32 i, t, numsamps, ofs;
ITPatternInfo *pInfo = calloc(1, sizeof(ITPatternInfo));
if (pInfo == NULL)
{
printf("Error: could not allocate memory for ITPatternInfo struct\n");
exit(1);
}
// START IT CLEANUP
if (fn == NULL)
{
printf("Error: no IT filename\n");
exit(1);
}
pInfo->Mask = 1;
pInfo->Note = 254; //note cut
// Stop all notes and loop back to the beginning
for (i = 0; i < 15; i++) // Save the last channel to put loop in
{
pInfo->Channel = (i + 1) | 128; // Channels are 1 based (Channels start at 1, not 0, ITTECH.TXT is WRONG) !!!
ITWritePattern(pInfo);
}
pInfo->Channel = (15 + 1) | 128;
pInfo->Mask = 9; // 1001 (note, special command)
pInfo->Command = 2; // Effect B: jump to...
pInfo->CommandValue = 0; //...order 0 (Loop to beginning)
ITWritePattern(pInfo);
free(pInfo);
while (ITcurrow++ < ITrows)
ITpattbuf[ITcurbuf][ITbufpos++] = 0; // end-of-row
ITpattlen[ITcurbuf++] = ITbufpos;
ITUpdate(); // Save the changes we just made
// END IT CLEANUP
f = fopen(fn, "wb");
if (f == NULL)
{
printf("Error: could not open IT file\n");
exit(1);
}
ITFileHeader *fHeader = calloc(1, sizeof(ITFileHeader));
if (fHeader == NULL)
{
printf("Error: could not allocate memory for ITFileHeader struct\n");
exit(1);
}
memcpy(fHeader->magic, "IMPM", 4);
if (SPCInfo->SongTitle[0])
strncpy(fHeader->songName, SPCInfo->SongTitle, 25);
else
strcpy(fHeader->songName, "spc2it conversion"); // default string
fHeader->OrderNumber = curpatt + 1; // number of orders + terminating order
for (numsamps = IT_SAMPLE_MAX; ITSamples[numsamps - 1] == NULL; numsamps--)
; // Count the number of samples (the reason of the minus one is because c arrays start at 0)
numsamps++;
fHeader->SampleNumber = numsamps; // Number of samples
fHeader->PatternNumber = curpatt; // Number of patterns
fHeader->TrackerCreatorVersion = 0xDAEB; // Created with this tracker version
fHeader->TrackerFormatVersion = 0x200; // Compatible with this tracker version
fHeader->Flags = 9; // Flags: Stereo, Linear Slides
fHeader->GlobalVolume = 128; // Global volume
fHeader->MixVolume = 100; // Mix volume
fHeader->InitialSpeed = 1; // Initial speed (fastest)
fHeader->InitialTempo = (u8)(SPCUpdateRate * 2.5); // Initial tempo (determined by update rate)
fHeader->PanningSeperation = 128; // Stereo separation (max)
for (i = 0; i < 8; i++)
fHeader->ChannelPan[i] = 0; // Channel pan: Set 8 channels to left
for (i = 8; i < 16; i++)
fHeader->ChannelPan[i] = 64; // Set 8 channels to right
for (i = 16; i < 64; i++)
fHeader->ChannelPan[i] = 128; // Disable the rest of the channels (Value: +128)
for (i = 0; i < 16; i++)
fHeader->ChannelVolume[i] = 64; // Channel Vol: set 16 channels loud
fwrite(fHeader, sizeof(ITFileHeader), 1, f);
free(fHeader);
// orders
for (i = 0; i < curpatt; i++)
fputc(i, f); // Write from 0 to the number of patterns (max: 0xFD)
fputc(255, f); // terminating order
// Sample offsets
ofs = sizeof(ITFileHeader) + (curpatt + 1) + ((numsamps * sizeof(s32)) + (curpatt * sizeof(s32)));
for (i = 0; i < numsamps; i++)
{
fwrite(&ofs, sizeof(s32), 1, f);
ofs += sizeof(ITFileSample);
if (ITSamples[i] != NULL) // Sample is going to be put in file? Add the length of the sample.
ofs += (ITSamples[i]->length * 2);
}
// Pattern offsets
for (i = 0; i < curpatt; i++)
{
t = offset[i] + ofs;
fwrite(&t, sizeof(s32), 1, f);
}
// samples
for (i = 0; i < numsamps; i++)
ITSSave(ITSamples[i], f);
// patterns
fwrite(ITPatterns, ITPatternsSize, 1, f);
for (i = 0; i < NUM_PATT_BUFS; i++)
free(ITpattbuf[i]);
free(ITPatterns);
fclose(f);
return 0;
}
void ITMix()
{
s32 envx, pitchslide, lvol = 0, rvol = 0, pitch, temp = 0, voice;
ITPatternInfo *pInfo = calloc(1, sizeof(ITPatternInfo));
if (pInfo == NULL)
{
printf("Error: could not allocate memory for ITPatternInfo struct\n");
exit(1);
}
u8 mastervolume = SPC_DSP[0x0C];
for (voice = 0; voice < 8; voice++)
{
if ((SPC_DSP[0x4C] & (1 << voice))) // 0x4C == key on
{
envx = SNDDoEnv(voice);
lvol = (envx >> 24) * (s32)((s8)SPC_DSP[(voice << 4) ]) * mastervolume >> 14; // Ext
rvol = (envx >> 24) * (s32)((s8)SPC_DSP[(voice << 4) + 0x01]) * mastervolume >> 14; // Ext
// Volume no echo: (s32)((s8)SPC_DSP[(voice << 4) ]) * mastervolume >> 7;
pitch = (s32)(*(u16 *)&SPC_DSP[(voice << 4) + 0x02]) * 7.8125; // This code merges 2 numbers together, high and low 8 bits, to make 16 bits.
// adjust for negative volumes
if (lvol < 0)
lvol = -lvol;
if (rvol < 0)
rvol = -rvol;
// lets see if we need to pitch slide
if (pitch && ITdata[voice].pitch)
{
pitchslide = (s32)(log2((f64)pitch / (f64)ITdata[voice].pitch) * 768.0);
if (pitchslide)
ITdata[voice].mask |= IT_MASK_PITCHSLIDE; // enable pitch slide
}
// adjust volume?
if ((lvol != ITdata[voice].lvol) || (rvol != ITdata[voice].rvol))
{
ITdata[voice].mask |= IT_MASK_ADJUSTVOLUME; // Enable adjust volume
ITdata[voice].lvol = lvol;
ITdata[voice].rvol = rvol;
}
}
pInfo->Channel = (voice + 1) | 128; //Channels here are 1 based!
pInfo->Mask = ITdata[voice].mask;
if (ITdata[voice].mask & IT_MASK_NOTE)
pInfo->Note = ITdata[voice].note;
if (ITdata[voice].mask & IT_MASK_SAMPLE)
pInfo->Sample = SPC_DSP[(voice << 4) + 4] + 1;
if (ITdata[voice].mask & IT_MASK_ADJUSTVOLUME)
pInfo->Volume = (lvol > 64) ? 64 : lvol;
if (ITdata[voice].mask & IT_MASK_PITCHSLIDE)
{
if (pitchslide > 0xF)
{
temp = pitchslide >> 2;
if (temp > 0xF)
temp = 0xF;
temp |= FINE_SLIDE;
ITdata[voice].pitch = (s32)((f64)ITdata[voice].pitch * pow(2, (f64)((temp & 0xF) << 2) / 768.0));
pInfo->Command = EFFECT_F;
pInfo->CommandValue = temp;
}
else if (pitchslide > 0)
{
temp = pitchslide | EXTRA_FINE_SLIDE;
ITdata[voice].pitch = (s32)((f64)ITdata[voice].pitch * pow(2, (f64)(temp & 0xF) / 768.0));
pInfo->Command = EFFECT_F;
pInfo->CommandValue = temp;
}
else if (pitchslide > -0x10)
{
temp = (-pitchslide) | EXTRA_FINE_SLIDE;
ITdata[voice].pitch = (s32)((f64)ITdata[voice].pitch * pow(2, (f64)(temp & 0xF) / -768.0));
pInfo->Command = EFFECT_E;
pInfo->CommandValue = temp;
}
else
{
temp = (-pitchslide) >> 2;
if (temp > 0xF)
temp = 0xF;
temp |= FINE_SLIDE;
ITdata[voice].pitch = (s32)((f64)ITdata[voice].pitch * pow(2, (f64)((temp & 0xF) << 2) / -768.0));
pInfo->Command = EFFECT_E;
pInfo->CommandValue = temp;
}
}
ITWritePattern(pInfo); // Write for left channel
pInfo->Channel = (voice + 8 + 1) | 128;
if (ITdata[voice].mask & IT_MASK_ADJUSTVOLUME)
pInfo->Volume = (rvol > 64) ? 64 : rvol;
ITWritePattern(pInfo); // Write for right channel
ITdata[voice].mask = 0; // Clear the mask
}
ITpattbuf[ITcurbuf][ITbufpos++] = 0; // End-of-row
if (++ITcurrow >= ITrows)
{
ITpattlen[ITcurbuf++] = ITbufpos;
ITbufpos = 0; // Reset buffer pos
ITcurrow = 0; // Reset current row
}
free(pInfo);
}

33
spctools/spc2it/it.h Normal file
View File

@@ -0,0 +1,33 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
#ifndef IT_H
#define IT_H
#define NUM_PATT_BUFS 128
#include "spc2ittypes.h"
s32 ITStart(s32); // Opens temp file, inits writing
s32 ITUpdate(); // Dumps pattern buffers to file
s32 ITWrite(char *fn); // Stops recording and writes IT file from temp data
void ITMix();
// Macros
#define FINE_SLIDE 0xF0
#define EXTRA_FINE_SLIDE 0xE0
#define EFFECT_F 6
#define EFFECT_E 5
#define IT_PATTERN_MAX 0xFD // The original Impulse Tracker has 200 patterns max
#define IT_SAMPLE_MAX 0xFF // The original Impulse Tracker has 99 samples max
#define IT_MASK_NOTE 1 // 0001 (Note)
#define IT_MASK_SAMPLE 2 // 0010 (Sample/instrument marker)
#define IT_MASK_ADJUSTVOLUME 4 // 0100 (volume/panning)
#define IT_MASK_NOTE_SAMPLE_ADJUSTVOLUME (IT_MASK_NOTE | IT_MASK_SAMPLE | IT_MASK_ADJUSTVOLUME)
#define IT_MASK_PITCHSLIDE 8 // 1000 (some special command, we use effect F and effect E)
#endif

171
spctools/spc2it/main.c Normal file
View File

@@ -0,0 +1,171 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>
#include <stdbool.h>
#include "sound.h"
#include "it.h"
#include "emu.h"
#include "sneese_spc.h"
#ifdef _WIN32
#undef realpath
#define realpath(N,R) _fullpath((R),(N),_MAX_PATH)
#endif
int main(int argc, char **argv)
{
size_t u8Size = sizeof(u8);
if (!(u8Size == 1))
printf("Warning: wrong size u8: %zu \n", u8Size);
size_t u16Size = sizeof(u16);
if (!(u16Size == 2))
printf("Warning: wrong size u16: %zu \n", u16Size);
size_t u32Size = sizeof(u32);
if (!(u32Size == 4))
printf("Warning: wrong size u32: %zu \n", u32Size);
size_t u64Size = sizeof(u64);
if (!(u64Size == 8))
printf("Warning: wrong size u64: %zu \n", u64Size);
size_t s8Size = sizeof(s8);
if (!(s8Size == 1))
printf("Warning: wrong size s8: %zu \n", s8Size);
size_t s16Size = sizeof(s16);
if (!(s16Size == 2))
printf("Warning: wrong size s16: %zu \n", s16Size);
size_t s32Size = sizeof(s32);
if (!(s32Size == 4))
printf("Warning: wrong size s32: %zu \n", s32Size);
size_t s64Size = sizeof(s64);
if (!(s64Size == 8))
printf("Warning: wrong size s64: %zu \n", s64Size);
size_t ITFileHeaderSize = sizeof(ITFileHeader);
if (!(ITFileHeaderSize == 192))
printf("Warning: wrong size ITFileHeader: %zu \n", ITFileHeaderSize);
size_t ITFileSampleSize = sizeof(ITFileSample);
if (!(ITFileSampleSize == 80))
printf("Warning: wrong size ITFileSample: %zu \n", ITFileSampleSize);
size_t ITFilePatternSize = sizeof(ITFilePattern);
if (!(ITFilePatternSize == 8))
printf("Warning: wrong size ITFilePattern: %zu \n", ITFilePatternSize);
size_t SPCFileSize = sizeof(SPCFile);
if (!(SPCFileSize == 65920))
printf("Warning: wrong size SPCFile: %zu \n", SPCFileSize);
s32 seconds, limit, ITrows;
char fn[PATH_MAX];
s32 i;
fn[0] = 0;
ITrows = 200; // Default 200 IT rows/pattern
limit = 0; // Will be set later
for (i = 1; i < argc; i++)
{
if (argv[i][0] == '-')
switch (argv[i][1])
{
case 'r':
i++;
ITrows = atoi(argv[i]);
break;
case 't':
i++;
limit = atoi(argv[i]);
break;
default:
printf("Warning: unrecognized option '-%c'\n", argv[i][1]);
}
else
realpath(argv[i], fn);
}
if (fn[0] == 0)
{
printf(" SPC2IT - converts SPC700 sound files to the Impulse Tracker format\n\n");
printf(" Usage: spc2it [options] <filename>\n");
printf(" Where <filename> is any .spc or .sp# file\n\n");
printf(" Options: ");
printf("-t x Specify a time limit in seconds [60 default]\n");
printf(" -d xxxxxxxx Voices to disable (1-8) [none default]\n");
printf(" -r xxx Specify IT rows per pattern [200 default]\n");
exit(0);
}
printf("\n");
printf("Filepath: %s\n", fn);
if (ITStart(ITrows))
{
printf("Error: failed to initialize pattern buffers\n");
exit(1);
}
if (SPCInit(fn)) // Reset SPC and load state
{
printf("Error: failed to initialize emulation\n");
exit(1);
}
if (SNDInit())
{
printf("Error: failed to initialize sound\n");
exit(1);
}
if ((!limit) && (SPCtime))
limit = SPCtime;
else if (!limit)
limit = 60;
printf("Time (seconds): %i\n", limit);
printf("IT Parameters:\n");
printf(" Rows/pattern: %d\n", ITrows);
printf("ID info:\n");
printf(" Song: %s\n", SPCInfo->SongTitle);
printf(" Game: %s\n", SPCInfo->GameTitle);
printf(" Dumper: %s\n", SPCInfo->DumperName);
printf(" Comments: %s\n", SPCInfo->Comment);
printf(" Created on: %s\n", SPCInfo->Date);
printf("\n");
fflush(stdout);
SNDNoteOn(SPC_DSP[0x4c]);
seconds = SNDratecnt = 0;
while (true)
{
ITMix();
if (ITUpdate())
break;
SNDratecnt += 1;
SPC_START(2048000 / (SPCUpdateRate * 2)); // emulate the SPC700
if (SNDratecnt >= SPCUpdateRate)
{
SNDratecnt -= SPCUpdateRate;
seconds++; // count number of seconds
printf("Progress: %f%%\r", (((f64)seconds / limit) * 100));
fflush(stdout);
if (seconds == limit)
break;
}
}
printf("\n\nSaving file...\n");
for (i = 0; i < PATH_MAX; i++)
if (fn[i] == 0)
break;
for (; i > 0; i--)
if (fn[i] == '.')
{
strcpy(&fn[i + 1], "it");
break;
}
if (ITWrite(fn))
printf("Error: failed to write %s.\n", fn);
else
printf("Wrote to %s successfully.\n", fn);
Reset_SPC();
return 0;
}

View File

@@ -0,0 +1,134 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
/**************************************************************************
Copyright (c) 2005 Brad Martin.
Some portions copyright (c) 1998-2005 Charles Bilyue'.
This file is part of OpenSPC.
sneese_spc.h: This file defines the interface between the SNEeSe SPC700
core and the associated wrapper files. As the licensing rights for SNEeSe
are different from the rest of OpenSPC, none of the files in this directory
are LGPL. Although this file was created by me (Brad Martin), it contains
some code derived from SNEeSe and therefore falls under its license. See
the file './doc/LICENSE_SNEESE' in this directory for more information.
**************************************************************************/
#if !defined(_SNEESE_SPC_H)
#define _SNEESE_SPC_H
#include "spc2ittypes.h"
/*========== DEFINES ==========*/
#define SPC_CTRL (SPCRAM[0xF1])
#define SPC_DSP_ADDR (SPCRAM[0xF2])
/*========== TYPES ==========*/
typedef union
{
u16 w;
struct
{
u8 l;
u8 h;
} b;
} word_2b;
typedef struct
{
u8 B_flag;
u8 C_flag;
u8 H_flag;
u8 I_flag;
u8 N_flag;
u8 P_flag;
u8 PSW;
u8 SP;
u8 V_flag;
u8 X;
u8 Z_flag;
u8 cycle;
u8 data;
u8 data2;
u8 opcode;
u8 offset;
word_2b PC;
word_2b YA;
word_2b address;
word_2b address2;
word_2b data16;
word_2b direct_page;
u32 Cycles;
void *FFC0_Address;
u32 TotalCycles;
s32 WorkCycles;
u32 last_cycles;
u8 PORT_R[4];
u8 PORT_W[4];
struct
{
u8 counter;
s16 position;
s16 target;
u32 cycle_latch;
} timers[4];
} SPC700_CONTEXT;
/*========== VARIABLES ==========*/
/* SPCimpl.c variables */
extern u8 In_CPU;
extern u32 Map_Address;
extern u32 Map_Byte;
extern u32 SPC_CPU_cycle_divisor;
extern u32 SPC_CPU_cycle_multiplicand;
extern u32 SPC_CPU_cycles;
extern u32 SPC_CPU_cycles_mul;
extern u8 SPC_DSP[256];
extern u32 SPC_DSP_DATA;
extern u8 SPCRAM[65536];
extern u32 sound_cycle_latch;
/* spc700.c variables */
extern SPC700_CONTEXT *active_context;
/*========== MACROS ==========*/
#define Wrap_SDSP_Cyclecounter()
#define update_sound()
#define BIT(bit) (1 << (bit))
/*========== PROCEDURES ==========*/
/* SPCimpl.c procedures */
void DisplaySPC(void);
void InvalidSPCOpcode(void);
void SPC_READ_DSP(void);
void SPC_WRITE_DSP(void);
/* spc700.c procedures */
void Reset_SPC(void);
u8 SPC_READ_PORT_W(u16 address);
void SPC_START(u32 cycles);
void SPC_WRITE_PORT_R(u16 address, u8 data);
u8 get_SPC_PSW(void);
void spc_restore_flags(void);
#endif

263
spctools/spc2it/sound.c Normal file
View File

@@ -0,0 +1,263 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
#include <math.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "emu.h"
#include "sound.h"
sndvoice SNDvoices[8];
s32 SNDratecnt;
static const u32 C[0x20] = {
0x0, 0x20000, 0x18000, 0x14000, 0x10000, 0xC000, 0xA000, 0x8000, 0x6000, 0x5000, 0x4000,
0x3000, 0x2800, 0x2000, 0x1800, 0x1400, 0x1000, 0xC00, 0xA00, 0x800, 0x600, 0x500,
0x400, 0x300, 0x280, 0x200, 0x180, 0x140, 0x100, 0xC0, 0x80, 0x40}; // How many cycles till adjust
// ADSR/GAIN
// PUBLIC (non-static) functions:
s32 SNDDoEnv(s32 voice)
{
u32 envx, c;
envx = SNDvoices[voice].envx;
for (;;)
{
u32 cyc = TotalCycles - SNDvoices[voice].envcyc;
switch (SNDvoices[voice].envstate)
{
case ATTACK:
c = C[(SNDvoices[voice].ar << 1) + 1];
if (c == 0)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = envx;
}
if (cyc > c)
{
SNDvoices[voice].envcyc += c;
envx += 0x2000000; // add 1/64th
if (envx >= 0x7F000000)
{
envx = 0x7F000000;
if (SNDvoices[voice].sl != 7)
SNDvoices[voice].envstate = DECAY;
else
SNDvoices[voice].envstate = SUSTAIN;
}
}
else
return SNDvoices[voice].envx = envx;
break;
case DECAY:
c = C[(SNDvoices[voice].dr << 1) + 0x10];
if (c == 0)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = envx;
}
if (cyc > c)
{
SNDvoices[voice].envcyc += c;
envx = (envx >> 8) * 255; // mult by 1-1/256
if (envx <= 0x10000000 * (SNDvoices[voice].sl + 1))
{
envx = 0x10000000 * (SNDvoices[voice].sl + 1);
SNDvoices[voice].envstate = SUSTAIN;
}
}
else
return SNDvoices[voice].envx = envx;
break;
case SUSTAIN:
c = C[SNDvoices[voice].sr];
if (c == 0)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = envx;
}
if (cyc > c)
{
SNDvoices[voice].envcyc += c;
envx = (envx >> 8) * 255; // mult by 1-1/256
}
else
return SNDvoices[voice].envx = envx;
break;
case RELEASE:
// says add 1/256?? That won't release, must be subtract.
// But how often? Oh well, who cares, I'll just
// pick a number. :)
c = C[0x1A];
if (c == 0)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = envx;
}
if (cyc > c)
{
SNDvoices[voice].envcyc += c;
envx -= 0x800000; // sub 1/256th
if ((envx == 0) || (envx > 0x7F000000))
{
SPC_DSP[0x4C] &= ~(1 << voice);
return SNDvoices[voice].envx = 0;
}
}
else
return SNDvoices[voice].envx = envx;
break;
case INCREASE:
c = C[SNDvoices[voice].gn];
if (c == 0)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = envx;
}
if (cyc > c)
{
SNDvoices[voice].envcyc += c;
envx += 0x2000000; // add 1/64th
if (envx > 0x7F000000)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = 0x7F000000;
}
}
else
return SNDvoices[voice].envx = envx;
break;
case DECREASE:
c = C[SNDvoices[voice].gn];
if (c == 0)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = envx;
}
if (cyc > c)
{
SNDvoices[voice].envcyc += c;
envx -= 0x2000000; // sub 1/64th
if (envx > 0x7F000000) // underflow
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = 0;
}
}
else
return SNDvoices[voice].envx = envx;
break;
case EXP:
c = C[SNDvoices[voice].gn];
if (c == 0)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = envx;
}
if (cyc > c)
{
SNDvoices[voice].envcyc += c;
envx = (envx >> 8) * 255; // mult by 1-1/256
}
else
return SNDvoices[voice].envx = envx;
break;
case BENT:
c = C[SNDvoices[voice].gn];
if (c == 0)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = envx;
}
if (cyc > c)
{
SNDvoices[voice].envcyc += c;
if (envx < 0x60000000)
envx += 0x2000000; // add 1/64th
else
envx += 0x800000; // add 1/256th
if (envx > 0x7F000000)
{
SNDvoices[voice].envcyc = TotalCycles;
return SNDvoices[voice].envx = 0x7F000000;
}
}
else
return SNDvoices[voice].envx = envx;
break;
case DIRECT:
SNDvoices[voice].envcyc = TotalCycles;
return envx;
}
}
}
void SNDNoteOn(u8 v)
{
s32 i, cursamp, adsr1, adsr2, gain;
v &= 0xFF;
for (i = 0; i < 8; i++)
if (v & (1 << i))
{
cursamp = SPC_DSP[4 + (i << 4)];
if (cursamp < 512)
{
SPC_DSP[0x4C] |= (1 << i);
// figure ADSR/GAIN
adsr1 = SPC_DSP[(i << 4) + 5];
if (adsr1 & 0x80)
{
// ADSR mode
adsr2 = SPC_DSP[(i << 4) + 6];
SNDvoices[i].envx = 0;
SNDvoices[i].envcyc = TotalCycles;
SNDvoices[i].envstate = ATTACK;
SNDvoices[i].ar = adsr1 & 0xF;
SNDvoices[i].dr = adsr1 >> 4 & 7;
SNDvoices[i].sr = adsr2 & 0x1f;
SNDvoices[i].sl = adsr2 >> 5;
}
else
{
// GAIN mode
gain = SPC_DSP[(i << 4) + 7];
if (gain & 0x80)
{
SNDvoices[i].envcyc = TotalCycles;
SNDvoices[i].envstate = gain >> 5;
SNDvoices[i].gn = gain & 0x1F;
}
else
{
SNDvoices[i].envx = (gain & 0x7F) << 24;
SNDvoices[i].envstate = DIRECT;
}
}
}
}
if (SPC_Write_DSP_Hook)
(*SPC_Write_DSP_Hook)(v);
}
void SNDNoteOff(u8 v)
{
s32 i;
for (i = 0; i < 8; i++)
if (v & (1 << i))
{
SNDDoEnv(i);
SNDvoices[i].envstate = RELEASE;
}
}
s32 SNDInit()
{
s32 i;
for (i = 0; i < 8; i++)
SNDvoices[i].envx = 0;
return (0);
}

28
spctools/spc2it/sound.h Normal file
View File

@@ -0,0 +1,28 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
#ifndef SOUND_H
#define SOUND_H
#include "spc2ittypes.h"
#define ATTACK 0 // A of ADSR
#define DECAY 1 // D of ADSR
#define SUSTAIN 2 // S of ADSR
#define RELEASE 3 // R of ADSR
#define DECREASE 4 // GAIN linear decrease mode
#define EXP 5 // GAIN exponential decrease mode
#define INCREASE 6 // GAIN linear increase mode
#define BENT 7 // GAIN bent line increase mode
#define DIRECT 8 // Directly specify ENVX
extern sndvoice SNDvoices[8];
extern s32 SNDkeys, SNDratecnt;
s32 SNDDoEnv(s32);
void SNDNoteOn(u8);
void SNDNoteOff(u8);
s32 SNDInit();
#endif

27
spctools/spc2it/spc2it.1 Normal file
View File

@@ -0,0 +1,27 @@
.TH spc2it "1" "September 10 2018" "User Commands"
.SH NAME
spc2it \- converts SPC700 sound files to the Impulse Tracker format
.SH SYNOPSIS
.B spc2it
.RI [ options ] " filename"
.TP
Where <filename> is any .spc or .sp# file
.SH DESCRIPTION
This manual describes the
.B spc2it
command. It converts SPC files to IT (Impulse Tracker) files.
.TP
Options:
.TP
\fB\-t\fR x
Specify a time limit in seconds [60 default]
.TP
\fB\-d\fR xxxxxxxx Voices to disable (1\-8) [none default]
.TP
\fB\-r\fR xxx
Specify IT rows per pattern [200 default]
.IP
.SH "SEE ALSO"
.BR adplay (1),
.BR schismtracker (1).
.br

View File

@@ -0,0 +1,155 @@
/****************************************************
*Part of SPC2IT, read readme.md for more information*
****************************************************/
#ifndef SPC2ITTYPES_H
#define SPC2ITTYPES_H
#include <stdint.h>
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef int8_t s8;
typedef int16_t s16;
typedef int32_t s32;
typedef int64_t s64;
typedef double f64;
typedef s16 pcm_t;
#define OneKB (1024)
#define Mem64k (OneKB * 64)
typedef struct
{
char magic[4]; // IMPM
char songName[26];
u16 PHiligt; // Pattern row hilight stuff
u16 OrderNumber; // Number of orders in song
u16 InstrumentNumber; // Number of instruments
u16 SampleNumber; // Number of samples
u16 PatternNumber; // Number of patterns
u16 TrackerCreatorVersion; // The version of the tracker that created the IT file
u16 TrackerFormatVersion; // Format version
u16 Flags; // Information flags
u16 Special; // Special st0ff
u8 GlobalVolume; // Global volume
u8 MixVolume; // Mix volume
u8 InitialSpeed; // Initial speed
u8 InitialTempo; // Initial tempo
u8 PanningSeperation; // Panning separation between channels
u8 PitchWheelDepth; // Pitch wheel depth for MIDI controllers
u16 MessageLength; // Length of message if Bit 0 of Special is 1
u32 MessageOffset; // Offset of message if Bit 0 of Special is 1
u32 Reserved; // Reserved stuff
u8 ChannelPan[64]; // Channel pan
u8 ChannelVolume[64]; // Channel volume
} ITFileHeader;
typedef struct
{
char magic[4]; // IMPS
char fileName[13]; // 8.3 DOS filename (including null termating)
u8 GlobalVolume; // Global volume for sample
u8 Flags;
u8 Volume; // Default volume
char SampleName[26];
u8 Convert;
u8 DefaultPan;
u32 NumberOfSamples; // not bytes! in samples!
u32 LoopBeginning; // not bytes! in samples!
u32 LoopEnd; // not bytes! in samples!
u32 C5Speed; // Num of bytes a second for C-5
u32 SustainLoopBeginning;
u32 SustainLoopEnd;
u32 SampleOffset; // the offset of the sample in file
u8 VibratoSpeed;
u8 VibratoDepth;
u8 VibratoRate;
u8 VibratoType;
} ITFileSample;
typedef struct
{
u16 Length;
u16 Rows;
u8 Padding[4];
} ITFilePattern;
typedef struct
{
u8 PC[2]; // Don't change this to u16, because of the way structs work, that will misalign the struct with the file!
u8 A;
u8 X;
u8 Y;
u8 PSW;
u8 SP;
u8 Reserved[2];
} SPCFileRegisters;
typedef struct
{
char SongTitle[32];
char GameTitle[32];
char DumperName[16];
char Comment[32];
char Date[11];
char SongLength[3];
char FadeLength[5];
char Artist[32];
u8 ChannelDisabled;
u8 EmulatorDumpedWith; //0 unknown, 1 ZSNES, 2 Snes9x
u8 Reserved[45];
} SPCFileInformation;
typedef struct
{
char FileTag[33]; // SNES-SPC700 Sound File Data v0.30
u8 FileTagTerminator[2]; // 0x1A, 0x1A
u8 ContainsID666; // 0x1A for contains ID666, 0x1B for no ID666
u8 Version; //Version minor (30)
SPCFileRegisters Registers; // 9bytes
SPCFileInformation Information; // 163bytes
u8 RAM[65536];
u8 DSPBuffer[128];
} SPCFile;
typedef struct
{
u8 Channel;
u8 Mask;
u8 Note;
u8 Sample;
u8 Volume;
u8 Command;
u8 CommandValue;
} ITPatternInfo;
typedef struct
{
s32 mask, pitch, lvol, rvol;
u8 note;
} itdata;
typedef struct
{
s32 length;
s32 loopto;
s16 *buf;
s32 freq;
} sndsamp;
typedef struct
{
s32 ave;
u32 envx, envcyc;
s32 envstate;
u32 ar, dr, sl, sr, gn;
} sndvoice;
#endif

3526
spctools/spc2it/spc700.c Normal file

File diff suppressed because it is too large Load Diff