mirror of
https://github.com/ScrelliCopter/VGM-Tools
synced 2025-02-21 04:09:25 +11:00
Compare commits
61 Commits
v0.1.0
...
effcf727ac
| Author | SHA1 | Date | |
|---|---|---|---|
| effcf727ac | |||
| 78790991b6 | |||
| 47f6b23943 | |||
| 5b28c18472 | |||
| 70bbf3d0d1 | |||
| 9f6c0664ff | |||
| 4013d1809c | |||
| 802bdef961 | |||
| ae14868953 | |||
| 46c78c24e1 | |||
| ff41b5415e | |||
| 7764375ec9 | |||
| e334ad82cc | |||
| 353d4e5def | |||
| 9c5e19264b | |||
| 111f800c49 | |||
| dbce8e5c29 | |||
| c1f36bd322 | |||
| 9946144995 | |||
| 05008f5c47 | |||
| f70bcd0a50 | |||
| 7c9c2464cb | |||
| 99bf061438 | |||
| ac89564669 | |||
| 9cdae38cbf | |||
| 5a059441df | |||
| a8cd2f0f36 | |||
| 695f6a1bf1 | |||
| 862639b4fe | |||
| 5cfb861369 | |||
| 6456404bd3 | |||
| 08b61568e1 | |||
| a76bb43ec1 | |||
| 2a654f25e8 | |||
| 47df3e2177 | |||
| 6510096f90 | |||
| 07ee0b546c | |||
| 454fcd3226 | |||
| c94016e793 | |||
| 172174c837 | |||
| 3fafdfd3f4 | |||
| 6ccf9f6e38 | |||
| b12258970e | |||
| 886966d7dc | |||
| 63aacb9a01 | |||
| 7c66bbab52 | |||
| fbf887b534 | |||
| 0a26bc8998 | |||
| 6d60ea91c0 | |||
| 6164533c1a | |||
| 7c5a577fba | |||
| 7e0740d03a | |||
| c1a5003a82 | |||
| 1969f7ae9a | |||
| 33851696b1 | |||
| 7aff052810 | |||
| 85a3e5089b | |||
| 508f464aa7 | |||
| 86317f57ec | |||
| d0945220a6 | |||
| 45ec9cf2cf |
22
.editorconfig
Normal file
22
.editorconfig
Normal 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
19
.gitignore
vendored
@@ -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
10
CMakeLists.txt
Normal 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
15
COPYING.zlib
Normal 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.
|
||||||
33
README.md
33
README.md
@@ -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.
|
|
||||||
|
|||||||
@@ -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
158
adpcm.c
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
16
adpcm.txt
16
adpcm.txt
@@ -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
|
|
||||||
|
|
||||||
@@ -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
445
adpcmb.c
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
8
common/CMakeLists.txt
Normal 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
68
common/endian.h
Normal 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
40
common/riffwriter.py
Normal 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
16
common/util.h
Normal 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
147
common/wave.c
Normal 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
48
common/wave.h
Normal 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
64
common/wavefile.c
Normal 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
110
common/wavesampler.py
Normal 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
18
common/waveserum.py
Normal 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
98
common/wavewriter.py
Normal 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
7
dsptools/CMakeLists.txt
Normal 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
261
dsptools/dspdecode.c
Normal 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;
|
||||||
|
}
|
||||||
18
dsptools/libdsptool/CMakeLists.txt
Normal file
18
dsptools/libdsptool/CMakeLists.txt
Normal 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)
|
||||||
21
dsptools/libdsptool/LICENSE
Normal file
21
dsptools/libdsptool/LICENSE
Normal 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.
|
||||||
101
dsptools/libdsptool/decode.c
Normal file
101
dsptools/libdsptool/decode.c
Normal 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;
|
||||||
|
}
|
||||||
61
dsptools/libdsptool/dsptool.h
Normal file
61
dsptools/libdsptool/dsptool.h
Normal 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
|
||||||
558
dsptools/libdsptool/encode.c
Normal file
558
dsptools/libdsptool/encode.c
Normal 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]);
|
||||||
|
}
|
||||||
69
dsptools/libdsptool/math.c
Normal file
69
dsptools/libdsptool/math.c
Normal 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
152
feropm.py
Executable 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()
|
||||||
@@ -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
24
neotools/CMakeLists.txt
Normal 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
46
neotools/README.md
Normal 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
88
neotools/adpcm.c
Normal 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
14
neotools/adpcm.h
Normal 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
279
neotools/adpcmb.c
Normal 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
25
neotools/adpcmb.h
Normal 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
57
neotools/libadpcma.c
Normal 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
92
neotools/libadpcmb.c
Normal 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
184
neotools/neoadpcmextract.c
Normal 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;
|
||||||
|
}
|
||||||
39
neotools/neoadpcmextract.h
Normal file
39
neotools/neoadpcmextract.h
Normal 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
78
sinharmonicswt.py
Normal 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
178
spctools/ripsamples.py
Executable 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)
|
||||||
10
spctools/spc2it/.clang-format
Normal file
10
spctools/spc2it/.clang-format
Normal 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
8
spctools/spc2it/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
obj/
|
||||||
|
build/
|
||||||
|
spc2it
|
||||||
|
*.exe
|
||||||
|
*.spc
|
||||||
|
*.it
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
16
spctools/spc2it/CMakeLists.txt
Normal file
16
spctools/spc2it/CMakeLists.txt
Normal 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
42
spctools/spc2it/Makefile
Normal 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
49
spctools/spc2it/README.md
Executable 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
|
||||||
160
spctools/spc2it/doc/LICENSE_SNEESE
Normal file
160
spctools/spc2it/doc/LICENSE_SNEESE
Normal 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
296
spctools/spc2it/doc/OPENSPC.TXT
Executable 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
|
||||||
180
spctools/spc2it/doc/Readme.linux
Normal file
180
spctools/spc2it/doc/Readme.linux
Normal 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
173
spctools/spc2it/doc/SOURCE.TXT
Executable 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
210
spctools/spc2it/emu.c
Normal 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
23
spctools/spc2it/emu.h
Normal 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
546
spctools/spc2it/it.c
Normal 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
33
spctools/spc2it/it.h
Normal 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
171
spctools/spc2it/main.c
Normal 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;
|
||||||
|
}
|
||||||
134
spctools/spc2it/sneese_spc.h
Normal file
134
spctools/spc2it/sneese_spc.h
Normal 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
263
spctools/spc2it/sound.c
Normal 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
28
spctools/spc2it/sound.h
Normal 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
27
spctools/spc2it/spc2it.1
Normal 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
|
||||||
155
spctools/spc2it/spc2ittypes.h
Normal file
155
spctools/spc2it/spc2ittypes.h
Normal 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
3526
spctools/spc2it/spc700.c
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user