[Audio 9/9] Loose ends (#1755)

* [Audio 9/9] Loose ends

* Fix sampleconv memset bug

* Doc updates from oot
This commit is contained in:
Tharo 2024-12-14 00:27:45 +00:00 committed by GitHub
parent 8efc382b9f
commit fc8d1165c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1835 additions and 963 deletions

View File

@ -166,6 +166,7 @@ SBC := tools/audio/sbc
SFC := tools/audio/sfc SFC := tools/audio/sfc
SFPATCH := tools/audio/sfpatch SFPATCH := tools/audio/sfpatch
ATBLGEN := tools/audio/atblgen ATBLGEN := tools/audio/atblgen
AFILE_SIZES := tools/audio/afile_sizes
# We want linemarkers in sequence assembly files for better assembler error messages # We want linemarkers in sequence assembly files for better assembler error messages
SEQ_CPP := $(CPP) -x assembler-with-cpp -fno-dollars-in-identifiers SEQ_CPP := $(CPP) -x assembler-with-cpp -fno-dollars-in-identifiers
SEQ_CPPFLAGS := -D_LANGUAGE_ASEQ -DMML_VERSION=MML_VERSION_MM -I include -I include/audio -I include/tables/sfx -I $(BUILD_DIR)/assets/audio/soundfonts SEQ_CPPFLAGS := -D_LANGUAGE_ASEQ -DMML_VERSION=MML_VERSION_MM -I include -I include/audio -I include/tables/sfx -I $(BUILD_DIR)/assets/audio/soundfonts
@ -723,8 +724,8 @@ $(BUILD_DIR)/assets/audio/soundfonts/%.o: $(BUILD_DIR)/assets/audio/soundfonts/%
$(LD) -r -T linker_scripts/soundfont.ld $(@:.o=.tmp) -o $(@:.o=.tmp2) $(LD) -r -T linker_scripts/soundfont.ld $(@:.o=.tmp) -o $(@:.o=.tmp2)
# patch defined symbols to be ABS symbols so that they remain file-relative offsets forever # patch defined symbols to be ABS symbols so that they remain file-relative offsets forever
$(SFPATCH) $(@:.o=.tmp2) $(@:.o=.tmp2) $(SFPATCH) $(@:.o=.tmp2) $(@:.o=.tmp2)
# write start and size symbols afterwards, filename != symbolic name so source symbolic name from the .name file written by sfc # write start and size symbols afterwards, filename != symbolic name so source symbolic name from the .name file written by sfc# also write a .note.name section containing the symbolic name of the soundfont
$(OBJCOPY) --add-symbol $$(cat $(<:.c=.name))_Start=.rodata:0,global --redefine-sym __LEN__=$$(cat $(<:.c=.name))_Size $(@:.o=.tmp2) $@ $(OBJCOPY) --add-symbol $$(cat $(<:.c=.name) | head -c -1)_Start=.rodata:0,global --redefine-sym __LEN__=$$(cat $(<:.c=.name) | head -c -1)_Size --add-section .note.name=$(<:.c=.name) $(@:.o=.tmp2) $@
# cleanup temp files # cleanup temp files
@$(RM) $(@:.o=.tmp) $(@:.o=.tmp2) @$(RM) $(@:.o=.tmp) $(@:.o=.tmp2)
$(RM_MDEBUG) $(RM_MDEBUG)
@ -784,6 +785,16 @@ $(BUILD_DIR)/src/audio/tables/%.o: src/audio/tables/%.c
$(BUILD_DIR)/assets/audio/sequence_font_table.o: $(BUILD_DIR)/assets/audio/sequence_font_table.s $(BUILD_DIR)/assets/audio/sequence_font_table.o: $(BUILD_DIR)/assets/audio/sequence_font_table.s
$(AS) $(ASFLAGS) $< -o $@ $(AS) $(ASFLAGS) $< -o $@
# make headers with file sizes and amounts
$(BUILD_DIR)/src/audio/session_config.o: $(BUILD_DIR)/assets/audio/soundfont_sizes.h $(BUILD_DIR)/assets/audio/sequence_sizes.h
$(BUILD_DIR)/assets/audio/soundfont_sizes.h: $(SOUNDFONT_O_FILES)
$(AFILE_SIZES) $@ NUM_SOUNDFONTS SOUNDFONT_SIZES .rodata $^
$(BUILD_DIR)/assets/audio/sequence_sizes.h: $(SEQUENCE_O_FILES)
$(AFILE_SIZES) $@ NUM_SEQUENCES SEQUENCE_SIZES .data $^
-include $(DEP_FILES) -include $(DEP_FILES)
# Print target for debugging # Print target for debugging

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/ --> <!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/ -->
<SampleBank Name="SampleBank_2" Index="2"> <SampleBank Name="SampleBank_2" Index="2">
<Sample Name="GoronDrum" FileName="goron_drum" Offset="0x000000" SampleRate="16000" BaseNote="C4"/> <Sample Name="GoronDrum" FileName="goron_drum" SampleRate="16000" BaseNote="C4"/>
<Sample Name="BassSlap" FileName="bass_slap" Offset="0x002540" SampleRate="32000" BaseNote="C4"/> <Sample Name="BassSlap" FileName="bass_slap" SampleRate="32000" BaseNote="C4"/>
<Sample Name="GoronOoh" FileName="goron_ooh" Offset="0x004800" SampleRate="22050" BaseNote="BF4"/> <Sample Name="GoronOoh" FileName="goron_ooh" SampleRate="22050" BaseNote="BF4"/>
<Sample Name="GoronOohHigh" FileName="goron_ooh_high" Offset="0x0072D0" SampleRate="16000" BaseNote="DF6"/> <Sample Name="GoronOohHigh" FileName="goron_ooh_high" SampleRate="16000" BaseNote="DF6"/>
<Sample Name="TomDrum" FileName="tom_drum" Offset="0x007B70" SampleRate="22050" BaseNote="B5"/> <Sample Name="TomDrum" FileName="tom_drum" SampleRate="22050" BaseNote="B5"/>
<Sample Name="ResonantBassMarimba" FileName="resonant_bass_marimba" Offset="0x008630" SampleRate="22050" BaseNote="A3"/> <Sample Name="ResonantBassMarimba" FileName="resonant_bass_marimba" SampleRate="22050" BaseNote="A3"/>
</SampleBank> </SampleBank>

View File

@ -0,0 +1,77 @@
# Samplebank XML Format Specification
Samplebank XMLs describe a samplebank file that contains compressed waveform data. It specifies which sample files to include as well as certain global properties such as the index of this samplebank.
---
```xml
<SampleBank
Name="<C Indentifier>"
Index="<uint>"
Medium="<Medium>"
CachePolicy="<CachePolicy>"
BufferBug="[bool]"
>
```
Begins a new samplebank.
**Attributes**
- **Name**: The name of the samplebank.
- **Index**: The index of the samplebank for the samplebank table. Must be a unique index for all samplebanks and pointers.
- **Medium**: The storage medium, from the `SampleMedium` enum.
- **CachePolicy**: The cache policy, from the `AudioCacheLoadType` enum.
- <ins>[Optional]</ins> **BufferBug**: Whether this samplebank suffers from a buffer clearing bug present in the original audio tools. For matching only.
**Tags**
-
```xml
<Pointer
Index="<uint>"
/>
```
Create an alternate index that refers to this samplebank.
**Attributes**
- **Index**: The alternative index, must be unique among all samplebanks and pointers.
---
-
```xml
<Sample
Name="<C Identifier>"
Path="<Path>"
/>
```
Adds a **compressed** sample file to the samplebank. The sample should be single-channel and big-endian, in a format that is recognizable by the audio driver such as: pcm16, vadpcm, or half-frame vadpcm.
**Attributes**
- **Name**: Name of this sample. Must be a valid C language identifier.
- **Path**: Path to aifc file relative to the project root (typically in `$(BUILD_DIR)/assets/audio/samples/`)
---
-
```xml
<Blob
Name="<C Identifier>"
Path="<Path>"
/>
```
Adds a binary blob to the samplebank. Intended for matching only when data cannot be identified.
**Attributes**
- **Name**: Name of this blob. Must be a valid C language identifier.
- **Path**: Path to binary file, relative to the project root (typically in `$(BUILD_DIR)/assets/audio/samples/`)
---
```xml
</SampleBank>
```
---

319
docs/audio/Soundfont_XML.md Normal file
View File

@ -0,0 +1,319 @@
# Soundfont XML Format Specification
Soundfont XMLs describe the layout of a single soundfont. These package raw samples together into instruments, of which there are three kinds:
- **Effects**: These are simple sound effects that just play a single sample without any modulation.
- **Drums**: These define a MIDI-style percussion key map.
- **Instruments**: These are instruments that may be played at any key with up to three voices and may be modulated by an envelope.
In the specification, `Note Name`s can be either a MIDI note name e.g. `C4` or it may be a **Zelda64** note number, which are related to MIDI note numbers ($n$) by $(n - 21) \mod 128$.
---
```xml
<Soundfont
Name="<C Identifier>"
Index="<uint>"
Medium="<Medium>"
CachePolicy="<CachePolicy>"
SampleBank="<Path>"
Indirect="[uint]"
SampleBankDD="[Path]"
IndirectDD="[uint]"
LoopsHaveFrames="[bool]"
PadToSize="[uint]"
NumInstruments="[uint]"
>
```
Begins a new soundfont.
**Attributes**
- **Name**: Soundfont symbol name. Must be a valid C identifier.
- **Index**: Soundfont index. Must be an integer.
- **Medium**: Storage medium. Must be an enum name from `SampleMedium`.
- **CachePolicy**: Cache policy. Must be an enum name from `AudioCacheLoadType`.
- **SampleBank**: Path to samplebank xml used by this soundfont.
- <ins>[Optional]</ins> **Indirect**: Pointer index if the samplebank is referenced indirectly.
- <ins>[Optional]</ins> **SampleBankDD**: Path to samplebank xml used for DD medium.
- <ins>[Optional]</ins> **IndirectDD**: Pointer index if the DD samplebank is referenced indirectly.
- <ins>[Optional]</ins> **LoopsHaveFrames**: Whether loops in this soundfont store the total frame count of the sample. Must be a boolean.
- <ins>[Optional]</ins> **PadToSize**: For matching only. Specifies the total file size the result output should be padded to.
- <ins>[Optional]</ins> **NumInstruments**: For matching only. Specifies the total number of instrument pointers. Usually this is automatically assigned based on `max(program_number) + 1` but some vanilla banks don't match this way.
**Tags**
-
```xml
<Envelopes>
```
Lists envelopes defined in this soundfont.
**Attributes**
N/A
**Tags**
-
```xml
<Envelope
Name="<C Identifier>"
Release="<u8>"
>
```
Starts a new envelope.
**Attributes**
- **Name**: Unique name for this envelope. Must be a valid C identifier.
- **Release**: Release rate index (into `gAudioCtx.adsrDecayTable`) for this envelope
**Tags**
-
```xml
<Point
Delay="<s16>"
Arg="<s16>"
/>
```
Add a point to the envelope at (delay, arg)
**Attributes**
- **Delay**: Duration until the next point
- **Arg**: Value of the envelope at this point
---
-
```xml
<Disable/>
```
Insert a ADSR_DISABLE command
---
-
```xml
<Hang/>
```
Insert a ADSR_HANG command
---
-
```xml
<Goto
Index="<uint>"
/>
```
Insert a ADSR_GOTO command
**Attributes**
- **Index**: Index of the envelope point to jump to
---
```xml
</Envelope>
```
---
```xml
</Envelopes>
```
---
-
```xml
<Samples
IsDD="[Bool]"
Cached="[Bool]"
>
```
Begins a list of samples used in this Soundfont.
**Attributes**
- <ins>[Optional]</ins> **IsDD**: Whether all the samples in the list are on the Disk Drive. The sample data will come from the samplebank `SampleBankDD`. **Default is `false`.** **NOTE this is not fully implemented, it should always be `false`.**
- <ins>[Optional]</ins> **Cached**: Whether all the samples in the list should be added to the `usedSamples` cache. **Default is `false`.**
**Tags**
-
```xml
<Sample
Name="<C Identifier>"
SampleRate="[Sample Rate]"
BaseNote="[Note Name]"
IsDD="[Bool]"
Cached="[Bool]"
/>
```
Declares a sample used in this soundfont.
**Attributes**
- **Name**: The name of this sample. A sample with this name must be present in the samplebank used by the soundfont.
- <ins>[Optional]</ins> **SampleRate**: An overriding sample rate for this sample. **Default comes from the sample file.**
- <ins>[Optional]</ins> **BaseNote**: An overriding root key for this sample. **Default comes from the sample file.**
- <ins>[Optional]</ins> **IsDD**: Whether this sample is on the Disk Drive. The sample data will come from the samplebank `SampleBankDD`. **Default is `false`.** **NOTE this is not fully implemented, it should always be `false`.**
- <ins>[Optional]</ins> **Cached**: Whether this sample should be added to the `usedSamples` cache. **Default is `false`.**
---
```xml
</Samples>
```
---
-
```xml
<Effects>
```
Begins a list of sound effects to define for this soundfont. Sound effects correspond to simple sounds that cannot be played at different keys.
**Attributes**
N/A
**Tags**
-
```xml
<Effect
Name="<C Identifier>"
Sample="<Sample Name>"
SampleRate="[Sample Rate]"
BaseNote="[Note Name]"
/>
```
Defines a single sound effect.
**Attributes**
- **Name**: The name of the sound effect, the name is made available in sequence files in the form `SF{n}_{name}` where `n` is the index of this soundfont and `name` is this name. For example, if `n=0` and `name=ExampleEffect` the name to use in sequence files is `SF0_ExampleEffect`.
- **Sample**: The name of the sample associated with this effect.
- <ins>[Optional]</ins> **SampleRate**: An overriding sample rate for this effect. **Default comes from the sample definition.**
- <ins>[Optional]</ins> **BaseNote**: An overriding root key for this effect. **Default comes from the sample definition.**
---
```xml
</Effects>
```
---
-
```xml
<Drums>
```
Begins the percussion definitions for this soundfont. Percussion corresponds to the MIDI notion of percussion, where single samples are mapped across a range of keys.
**Attributes**
N/A
**Tags**
-
```xml
<Drum
Name="<C Identifier>"
Note="[Note Name]"
NoteStart="[Note Name]"
NoteEnd="[Note Name]"
Pan="<u8>"
Envelope="<Envelope Name>"
Release="[u8]"
Sample="<Sample Name>"
SampleRate="[Sample Rate]"
BaseNote="[Note Name]"
/>
```
Defines a single percussion range.
**Attributes**
- **Name**: The name of this sound. Definitions are emitted for sequence files in the form `SF{n}_{name}_{note}` for every note covered by this sound.
- <ins>[Optional]</ins> **Note**: The key to map this sound to. Should not overlap with other definitions. **If this field is left unspecified, `NoteStart` and `NoteEnd` become required.**
- <ins>[Optional]</ins> **NoteStart**: The first key that is mapped to this sound. Should not overlap with other definitions. **If this field is left unspecified, `Note` becomes required. If this field is specified, `NoteEnd` must also be specified.**
- <ins>[Optional]</ins> **NoteEnd**: The last key that is mapped to this sound. Should not overlap with other definitions. **If this field is left unspecified, `Note` becomes required. If this field is specified, `NoteStart` must also be specified.**
- **Pan**: The stereo weight for this sound. Center=`64`.
- **Envelope**: The envelope to modulate the volume over time with. Must be defined in the `Envelopes` list.
- <ins>[Optional]</ins> **Release**: An override for the envelope release rate. **Default is the release rate specified in the envelope definition**
- **Sample**: The name of the sample to use.
- <ins>[Optional]</ins> **SampleRate**: An overriding sample rate for this sound. **Default comes from the sample definition.**
- <ins>[Optional]</ins> **BaseNote**: An overriding root key for this sound. **Default comes from the sample definition.**
---
```xml
</Drums>
```
---
-
```xml
<Instruments>
```
Begins the instrument definitions for this soundfont. Instruments correspond to the MIDI notion of instruments, with up to 3 samples (voices) per instrument that must map to contiguous ranges of notes.
**Attributes**
N/A
**Tags**
-
```xml
<Instrument
ProgramNumber="<>"
Name="<C Identifier>"
Envelope="<Envelope Name>"
Release="[u8]"
Sample="<Sample Name>"
SampleRate="[Sample Rate]"
BaseNote="[Note Name]"
RangeLo="[Note Name]"
SampleLo="[Sample Name]"
SampleRateLo="[Sample Rate]"
BaseNoteLo="[Note Name]"
RangeHi="[Note Name]"
SampleHi="[Sample Name]"
SampleRateHi="[Sample Rate]"
BaseNoteHi="[Note Name]"
/>
```
Defines an instrument.
**Attributes**
- **ProgramNumber**: MIDI Program Number for this instrument. Must be in the range `0 <= n <= 125`
- **Name**: The name of this instrument.
- **Envelope**: Envelope to use, identified by name.
- <ins>[Optional]</ins> **Release**: Release rate index override. **Default release rate comes from the chosen envelope.**
- **Sample**: The name of the middle sample to use for this instrument.
- <ins>[Optional]</ins> **SampleRate**: Sample rate override for the middle sample. **Default is sourced from the sample properties.**
- <ins>[Optional]</ins> **BaseNote**: Base note override for the middle sample. **Default is sourced from the sample properties.**
- <ins>[Optional]</ins> **RangeLo**: The largest note for SampleLo. SampleLo will be used instead of Sample for keys in the range [0, RangeLo]. **If left unspecified, SampleLo must not be specified. If specified, SampleLo must be specified.**
- <ins>[Optional]</ins> **SampleLo**: The name of the low sample to use for this instrument.
- <ins>[Optional]</ins> **SampleRateLo**: Sample rate override for the low sample. **Default is sourced from the sample properties.**
- <ins>[Optional]</ins> **BaseNoteLo**: Base note override for the low sample. **Default is sourced from the sample properties.**
- <ins>[Optional]</ins> **RangeHi**: The smallest note for SampleHi. SampleHi will be used instead of Sample for keys in the range [RangeHi, 127]. **If left unspecified, SampleHi must not be specified. If specified, SampleHi must be specified.**
- <ins>[Optional]</ins> **SampleHi**: The name of the high sample to use for this instrument.
- <ins>[Optional]</ins> **SampleRateHi**: Sample rate override for the high sample. **Default is sourced from the sample properties.**
- <ins>[Optional]</ins> **BaseNoteHi**: Base note override for the high sample. **Default is sourced from the sample properties.**
---
```xml
</Instruments>
```
---
```xml
</Soundfont>
```
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -556,8 +556,8 @@ _RESET_SECTION
.macro .startseq name .macro .startseq name
/* Begin a sequence. */ /* Begin a sequence. */
/* Write the sequence name into a special .name section */ /* Write the sequence name into a special .note.name section */
.pushsection .name, "", @note .pushsection .note.name, "", @note
.asciz "\name" .asciz "\name"
.balign 4 .balign 4
.popsection .popsection

View File

@ -3,9 +3,9 @@
// Static/compile-time assertions // Static/compile-time assertions
#if (__STDC_VERSION__ >= 202311L) #if !defined(__sgi) && (__STDC_VERSION__ >= 202311L)
// static_assert is a keyword in C23, do not define it // static_assert is a keyword in C23, do not define it
#elif (__STDC_VERSION__ >= 201112L) #elif !defined(__sgi) && (__STDC_VERSION__ >= 201112L)
# define static_assert(cond, msg) _Static_assert(cond, msg) # define static_assert(cond, msg) _Static_assert(cond, msg)
#else #else
# ifndef GLUE # ifndef GLUE

View File

@ -2,6 +2,7 @@
#define SFX_H #define SFX_H
#include "PR/ultratypes.h" #include "PR/ultratypes.h"
#include "libc/assert.h"
#include "z64math.h" #include "z64math.h"
/** /**
@ -23,25 +24,50 @@
typedef enum SfxId { typedef enum SfxId {
NA_SE_NONE, // Requesting a sfx with this id will play no sound NA_SE_NONE, // Requesting a sfx with this id will play no sound
NA_SE_PL_BASE = 0x7FF, NA_SE_PL_BASE = 0x7FF,
#include "tables/sfx/playerbank_table.h" #include "tables/sfx/playerbank_table.h"
NA_SE_PL_END,
NA_SE_IT_BASE = 0x17FF, NA_SE_IT_BASE = 0x17FF,
#include "tables/sfx/itembank_table.h" #include "tables/sfx/itembank_table.h"
NA_SE_IT_END,
NA_SE_EV_BASE = 0x27FF, NA_SE_EV_BASE = 0x27FF,
#include "tables/sfx/environmentbank_table.h" #include "tables/sfx/environmentbank_table.h"
NA_SE_EV_END,
NA_SE_EN_BASE = 0x37FF, NA_SE_EN_BASE = 0x37FF,
#include "tables/sfx/enemybank_table.h" #include "tables/sfx/enemybank_table.h"
NA_SE_EN_END,
NA_SE_SY_BASE = 0x47FF, NA_SE_SY_BASE = 0x47FF,
#include "tables/sfx/systembank_table.h" #include "tables/sfx/systembank_table.h"
NA_SE_SY_END,
NA_SE_OC_BASE = 0x57FF, NA_SE_OC_BASE = 0x57FF,
#include "tables/sfx/ocarinabank_table.h" #include "tables/sfx/ocarinabank_table.h"
NA_SE_OC_END,
NA_SE_VO_BASE = 0x67FF, NA_SE_VO_BASE = 0x67FF,
#include "tables/sfx/voicebank_table.h" #include "tables/sfx/voicebank_table.h"
NA_SE_VO_END,
NA_SE_MAX NA_SE_MAX
} SfxId; } SfxId;
#undef DEFINE_SFX #undef DEFINE_SFX
// These limits are due to the way Sequence 0 is programmed. There is also a global limit of 1024 entries for every bank
// enforced in Audio_PlayActiveSfx in sfx.c
static_assert(NA_SE_PL_END - (NA_SE_PL_BASE + 1) <= 512, "Player Bank SFX Table is limited to 512 entries due to Sequence 0");
static_assert(NA_SE_IT_END - (NA_SE_IT_BASE + 1) <= 128, "Item Bank SFX Table is limited to 128 entries due to Sequence 0");
static_assert(NA_SE_EV_END - (NA_SE_EV_BASE + 1) <= 512, "Environment Bank SFX Table is limited to 512 entries due to Sequence 0");
static_assert(NA_SE_EN_END - (NA_SE_EN_BASE + 1) <= 768, "Enemy Bank SFX Table is limited to 768 entries due to Sequence 0");
static_assert(NA_SE_SY_END - (NA_SE_SY_BASE + 1) <= 128, "System Bank SFX Table is limited to 128 entries due to Sequence 0");
static_assert(NA_SE_OC_END - (NA_SE_OC_BASE + 1) <= 128, "Ocarina Bank SFX Table is limited to 128 entries due to Sequence 0");
static_assert(NA_SE_VO_END - (NA_SE_VO_BASE + 1) <= 512, "Voice Bank SFX Table is limited to 512 entries due to Sequence 0");
typedef enum SfxPauseMenu { typedef enum SfxPauseMenu {
/* 0 */ SFX_PAUSE_MENU_CLOSE, /* 0 */ SFX_PAUSE_MENU_CLOSE,
/* 1 */ SFX_PAUSE_MENU_OPEN /* 1 */ SFX_PAUSE_MENU_OPEN

View File

@ -4,14 +4,15 @@ OUTPUT_ARCH (mips)
SECTIONS { SECTIONS {
.rodata : .rodata ALIGN(16) :
{ {
*(.data*) *(.data*)
*(.rodata*) *(.rodata*)
. = ALIGN(16); . = ALIGN(16);
__LEN__ = . - ADDR(.rodata);
} }
__LEN__ = ABSOLUTE(SIZEOF(.rodata));
/DISCARD/ : /DISCARD/ :
{ {
*(*); *(*);

View File

@ -1,5 +1,10 @@
#include "global.h" #include "global.h"
#include "buffers.h" #include "buffers.h"
#include "assets/audio/sequence_sizes.h"
#include "assets/audio/soundfont_sizes.h"
#define SFX_SEQ_SIZE Sequence_0_SIZE
#define AMBIENCE_SEQ_SIZE Sequence_1_SIZE
#define SFX_SOUNDFONTS_SIZE (Soundfont_0_SIZE + Soundfont_1_SIZE + Soundfont_2_SIZE)
static s32 sBssPad[36]; static s32 sBssPad[36];
AudioContext gAudioCtx; AudioContext gAudioCtx;
@ -14,21 +19,12 @@ const s16 gAudioTatumInit[] = {
TATUMS_PER_BEAT, // gTatumsPerBeat TATUMS_PER_BEAT, // gTatumsPerBeat
}; };
// TODO: Extract from table?
#define NUM_SOUNDFONTS 41
#define SFX_SEQ_SIZE 0xC6A0
#define AMBIENCE_SEQ_SIZE 0xFC0
#define SOUNDFONT_0_SIZE 0x81C0
#define SOUNDFONT_1_SIZE 0x36D0
#define SOUNDFONT_2_SIZE 0xCE0
// Sizes of everything on the init pool // Sizes of everything on the init pool
#define AI_BUFFERS_SIZE (AIBUF_SIZE * ARRAY_COUNT(gAudioCtx.aiBuffers)) #define AI_BUFFERS_SIZE (AIBUF_SIZE * ARRAY_COUNT(gAudioCtx.aiBuffers))
#define SOUNDFONT_LIST_SIZE (NUM_SOUNDFONTS * sizeof(SoundFont)) #define SOUNDFONT_LIST_SIZE (NUM_SOUNDFONTS * sizeof(SoundFont))
// 0x19BD0 // 0x19BD0
#define PERMANENT_POOL_SIZE \ #define PERMANENT_POOL_SIZE (SFX_SEQ_SIZE + AMBIENCE_SEQ_SIZE + SFX_SOUNDFONTS_SIZE + 0x430)
(SFX_SEQ_SIZE + AMBIENCE_SEQ_SIZE + SOUNDFONT_0_SIZE + SOUNDFONT_1_SIZE + SOUNDFONT_2_SIZE + 0x430)
const AudioHeapInitSizes gAudioHeapInitSizes = { const AudioHeapInitSizes gAudioHeapInitSizes = {
ALIGN16(sizeof(gAudioHeap) - 0x100), // audio heap size ALIGN16(sizeof(gAudioHeap) - 0x100), // audio heap size

View File

@ -4,6 +4,7 @@
* Description: Twinmold * Description: Twinmold
*/ */
#include "prevent_bss_reordering.h"
#include "z_boss_02.h" #include "z_boss_02.h"
#include "z64rumble.h" #include "z64rumble.h"
#include "z64shrink_window.h" #include "z64shrink_window.h"

View File

@ -1,5 +1,6 @@
__pycache__/ __pycache__/
afile_sizes
atblgen atblgen
sfpatch sfpatch
sbc sbc

View File

@ -1,4 +1,4 @@
PROGRAMS := atblgen sfpatch sbc sfc PROGRAMS := afile_sizes atblgen sbc sfc sfpatch
ifeq ($(shell which xml2-config),) ifeq ($(shell which xml2-config),)
$(error xml2-config not found. Did you install libxml2-dev?) $(error xml2-config not found. Did you install libxml2-dev?)
@ -9,7 +9,7 @@ FORMAT_ARGS := -i -style=file
CC := gcc CC := gcc
CFLAGS := -Wall -Wextra -pedantic CFLAGS := -Wall -Wextra -pedantic
OPTFLAGS := -Og -g3 OPTFLAGS := -O2
XML_CFLAGS := $(shell xml2-config --cflags) XML_CFLAGS := $(shell xml2-config --cflags)
XML_LDFLAGS := $(shell xml2-config --libs) XML_LDFLAGS := $(shell xml2-config --libs)
@ -30,10 +30,11 @@ format:
$(CLANG_FORMAT) $(FORMAT_ARGS) $(shell find . -maxdepth 1 -type f -name "*.[ch]") $(CLANG_FORMAT) $(FORMAT_ARGS) $(shell find . -maxdepth 1 -type f -name "*.[ch]")
$(MAKE) -C sampleconv format $(MAKE) -C sampleconv format
atblgen_SOURCES := audio_tablegen.c samplebank.c soundfont.c xml.c util.c afile_sizes_SOURCES := afile_sizes.c util.c
sfpatch_SOURCES := sfpatch.c util.c atblgen_SOURCES := audio_tablegen.c samplebank.c soundfont.c xml.c util.c
sbc_SOURCES := samplebank_compiler.c samplebank.c aifc.c xml.c util.c sbc_SOURCES := samplebank_compiler.c samplebank.c aifc.c xml.c util.c
sfc_SOURCES := soundfont_compiler.c samplebank.c soundfont.c aifc.c xml.c util.c sfc_SOURCES := soundfont_compiler.c samplebank.c soundfont.c aifc.c xml.c util.c
sfpatch_SOURCES := sfpatch.c util.c
atblgen_CFLAGS := $(XML_CFLAGS) atblgen_CFLAGS := $(XML_CFLAGS)
sbc_CFLAGS := $(XML_CFLAGS) sbc_CFLAGS := $(XML_CFLAGS)

59
tools/audio/README.md Normal file
View File

@ -0,0 +1,59 @@
# Z64 Audio Tools
The Z64 Audio Tools work together to implement the full audio asset pipeline
![](../../docs/audio/build_flowchart.png)
**Licensing Information**
* The programs `atblgen`, `sampleconv`, `sbc` and `sfc` are (mostly) distributed under MPL-2.0. The VADPCM encoding and decoding portions of `sampleconv` are under CC0-1.0.
* The programs `sfpatch` and `afile_sizes` are distributed under CC0-1.0.
* The extraction tool is distributed under CC0-1.0.
## sampleconv
Converts aifc <-> aiff / wav
Used in extraction and build to convert audio sample data between uncompressed mono 16-bit PCM and the compressed formats used by the audio driver.
## SampleBank Compiler (sbc)
Converts samplebank xml + aifc -> asm
Samplebanks are converted to assembly files for building as it is easier to define the necessary absolute symbols, and they are pure unstructured data.
## SoundFont Compiler (sfc)
Converts soundfont & samplebank xml + aifc -> C
Soundfonts are converted to C rather than assembly as it shares data structures with the audio driver code. Modifying the structures used by the driver without updating `sfc` to write them should error at compile-time rather than crash at runtime.
## sfpatch
`Usage: sfpatch in.elf out.elf`
This tool patches the symbol table of an ELF file (`in.elf`) to make every defined symbol in the file an absolute symbol. This is a required step for building soundfonts from C source as all pointers internal to a soundfont are offset from the start of the soundfont file and not the audiobank segment as a whole. Making all defined symbols ABS symbols prevents the linker from updating their values later, ensuring they remain file-relative.
## atblgen
Generates various audio code tables.
- Samplebank table: Specifies where in the `Audiotable` file each samplebank begins and how large it is.
- Soundfont table: Specifies where in the `Audiobank` files each soundfont begins, how large it is, which samplebanks it uses, and how many instruments/drums/sfx it contains.
- Sequence font table: Contains information on what soundfonts each sequence uses. Generated from the sequence object files that embed a `.note.fonts` section that holds this information.
The sequence table is not generated as some things in that table are better left manually specified, such as sequence enum names and flags. This also lets us have the sequence table before assembling any sequence files which is nice for some sequence commands like `runseq`.
## afile_sizes
Produces header files containing binary file sizes for a given set of object files. Used to produce headers containing soundfont and sequence files and the number of each for use in code files.
## extraction
This collection of python files implements the extraction of audio data from a base ROM.
Files that are designed to be used externally include:
- `audio_extract.py` is the main file for audio extraction, it expects an external script to call `extract_audio_for_version` with the necessary inputs.
- `disassemble_sequence.py` is runnable but is not used in this way in either extraction or building. It may be used to manually disassemble a sequence binary.
- `tuning.py` is runnable but is not used that way in either extraction or building. It may be used to manually determine alternative matches for the samplerate and basenote of a sample as the extraction procedure cannot always determine these uniquely.
See individual python source files for further details on their purposes.

120
tools/audio/afile_sizes.c Normal file
View File

@ -0,0 +1,120 @@
/* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */
/* SPDX-License-Identifier: CC0-1.0 */
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include "elf32.h"
#include "util.h"
static int
usage(const char *progname)
{
fprintf(stderr,
// clang-format off
"Generates a header containing definitions for the sizes of all the input object files and a" "\n"
"definition for the number of input files." "\n"
"Usage: %s <header output path> <num define> <header guard> <section name> <object files...>" "\n"
" header output path: Path to write the generated header to" "\n"
" num define: The name of the definition for the number of input files" "\n"
" header guard: The header guard definition name to be used for the output header" "\n"
" section name: The object file section to output the size of in each definition" "\n"
" object files: List of paths to each object file to be processed, each input object file" "\n"
" must contain the section requested in the section name argument and must" "\n"
" also contain a .note.name section containing the null-terminated symbolic " "\n"
" name of the object that is used to name the size definitions." "\n",
// clang-format on
progname);
return EXIT_FAILURE;
}
int
main(int argc, char **argv)
{
const char *progname = argv[0];
if (argc < 6) // progname, 4 required args, at least 1 input file
return usage(progname);
const char *header_out = argv[1];
const char *num_def = argv[2];
const char *header_guard = argv[3];
const char *secname = argv[4];
int num_files = argc - 5;
char **files = &argv[5];
// Open the header for writing, write the header guard
FILE *out = fopen(header_out, "w");
if (out == NULL)
error("failed to open output file \"%s\" for writing: %s", header_out, strerror(errno));
fprintf(out,
// clang-format off
"#ifndef %s_H_" "\n"
"#define %s_H_" "\n"
"\n",
// clang-format on
header_guard, header_guard);
// For each input elf file, write the size define
for (int i = 0; i < num_files; i++) {
const char *path = files[i];
size_t data_size;
void *data = elf32_read(path, &data_size);
Elf32_Shdr *shstrtab = elf32_get_shstrtab(data, data_size);
if (shstrtab == NULL)
error("Input file \"%s\" has no shstrtab?", path);
// Read in the .note.name section containing the object's symbolic name.
// We run this on both soundfonts and sequences:
// - Soundfont .note.name sections are added with objcopy
// - Sequence .note.name sections are assembled as part of .startseq
Elf32_Shdr *name_section = elf32_section_forname(".note.name", shstrtab, data, data_size);
if (name_section == NULL)
error("Input file \"%s\" has no name section?", path);
uint32_t name_section_offset = elf32_read32(name_section->sh_offset);
uint32_t name_section_size = elf32_read32(name_section->sh_size);
validate_read(name_section_offset, name_section_size, data_size);
const char *object_name = GET_PTR(data, name_section_offset);
if (strnlen(object_name, name_section_size + 1) >= name_section_size)
error("Input file \"%s\" name is not properly terminated?", path);
// Read the section header for the data we're interested in, the name is given in the program args
Elf32_Shdr *sec = elf32_section_forname(secname, shstrtab, data, data_size);
if (sec == NULL)
error("Input file \"%s\" has no section named \"%s\"?", path, secname);
// Assumption: The section size matches the size in the <object_name>_Size symbol, this is always
// true for soundfonts by nature of how they're built (cf. soundfont.ld) and should always be true
// of sequences since writing anything that results in output data before .startseq and after .endseq
// is essentially undefined.
size_t object_size = elf32_read32(sec->sh_size);
fprintf(out, "#define %s_SIZE 0x%lX\n", object_name, object_size);
free(data);
}
// Write the total number of input files, end the header
fprintf(out,
// clang-format off
"\n"
"#define %s %d" "\n"
"\n"
"#endif" "\n",
// clang-format on
num_def, num_files);
fclose(out);
return EXIT_SUCCESS;
}

View File

@ -534,14 +534,19 @@ aifc_read(aifc_data *af, const char *path, uint8_t *match_buf, size_t *match_buf
void void
aifc_dispose(aifc_data *af) aifc_dispose(aifc_data *af)
{ {
free(af->book_state); if (af->has_book) {
af->has_book = false; free(af->book_state);
af->has_book = false;
}
af->has_loop = false; af->has_loop = false;
free(af->compression_name); if (af->compression_name != NULL)
free(af->compression_name);
for (size_t i = 0; i < af->num_markers; i++) if (af->markers != NULL) {
free((*af->markers)[i].label); for (size_t i = 0; i < af->num_markers; i++)
free(af->markers); free((*af->markers)[i].label);
free(af->markers);
}
} }

View File

@ -424,11 +424,11 @@ tablegen_sequences(const char *seq_font_tbl_out, const char *seq_order_path, con
if (shstrtab == NULL) if (shstrtab == NULL)
error("ELF file \"%s\" has no section header string table?", path); error("ELF file \"%s\" has no section header string table?", path);
// The .fonts and .name sections are written when assembling the sequence: // The .note.fonts and .note.name sections are written when assembling the sequence:
// The .fonts section contains a list of bytes for each soundfont the sequences uses // The .note.fonts section contains a list of bytes for each soundfont the sequences uses
// The .name section contains the null-terminated name of the sequence as set by .startseq // The .note.name section contains the null-terminated name of the sequence as set by .startseq
Elf32_Shdr *font_section = elf32_section_forname(".fonts", shstrtab, data, data_size); Elf32_Shdr *font_section = elf32_section_forname(".note.fonts", shstrtab, data, data_size);
if (font_section == NULL) if (font_section == NULL)
error("Sequence file \"%s\" has no fonts section?", path); error("Sequence file \"%s\" has no fonts section?", path);
@ -436,7 +436,7 @@ tablegen_sequences(const char *seq_font_tbl_out, const char *seq_order_path, con
uint32_t font_section_size = elf32_read32(font_section->sh_size); uint32_t font_section_size = elf32_read32(font_section->sh_size);
validate_read(font_section_offset, font_section_size, data_size); validate_read(font_section_offset, font_section_size, data_size);
Elf32_Shdr *name_section = elf32_section_forname(".name", shstrtab, data, data_size); Elf32_Shdr *name_section = elf32_section_forname(".note.name", shstrtab, data, data_size);
if (name_section == NULL) if (name_section == NULL)
error("Sequence file \"%s\" has no name section?", path); error("Sequence file \"%s\" has no name section?", path);

View File

@ -8,17 +8,18 @@ import os, shutil, time
from dataclasses import dataclass from dataclasses import dataclass
from multiprocessing.pool import ThreadPool from multiprocessing.pool import ThreadPool
from typing import Dict, List, Tuple, Union from typing import Dict, List, Tuple, Union
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from .audio_tables import AudioCodeTable, AudioCodeTableEntry, AudioStorageMedium from .audio_tables import AudioCodeTable, AudioCodeTableEntry, AudioStorageMedium
from .audiotable import AudioTableData, AudioTableFile, AudioTableSample from .audiotable import AudioTableData, AudioTableFile, AudioTableSample
from .audiobank_file import AudiobankFile from .audiobank_file import AudiobankFile
from .disassemble_sequence import CMD_SPEC, SequenceDisassembler, SequenceTableSpec, MMLVersion from .disassemble_sequence import CMD_SPEC, SequenceDisassembler, SequenceTableSpec, MMLVersion
from .util import align, debugm, error, incbin, program_get, XMLWriter from .extraction_xml import ExtractionDescription, SampleBankExtractionDescription, SoundFontExtractionDescription, SequenceExtractionDescription
from .util import align, incbin, program_get, XMLWriter
@dataclass @dataclass
class GameVersionInfo: class GameVersionInfo:
# Version Name
version_name : str
# Music Macro Language Version # Music Macro Language Version
mml_version : MMLVersion mml_version : MMLVersion
# Soundfont table code offset # Soundfont table code offset
@ -49,7 +50,7 @@ BASEROM_DEBUG = False
# ====================================================================================================================== # ======================================================================================================================
def collect_sample_banks(audiotable_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo, def collect_sample_banks(audiotable_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo,
table : AudioCodeTable, samplebank_xmls : Dict[int, Tuple[str, Element]]): table : AudioCodeTable, samplebank_descs : Dict[int, SampleBankExtractionDescription]):
sample_banks : List[Union[AudioTableFile, int]] = [] sample_banks : List[Union[AudioTableFile, int]] = []
for i,entry in enumerate(table): for i,entry in enumerate(table):
@ -72,7 +73,7 @@ def collect_sample_banks(audiotable_seg : memoryview, extracted_dir : str, versi
bug = i in version_info.audiotable_buffer_bugs bug = i in version_info.audiotable_buffer_bugs
bank = AudioTableFile(i, audiotable_seg, entry, table.rom_addr, buffer_bug=bug, bank = AudioTableFile(i, audiotable_seg, entry, table.rom_addr, buffer_bug=bug,
extraction_xml=samplebank_xmls.get(i, None)) extraction_desc=samplebank_descs.get(i, None))
if BASEROM_DEBUG: if BASEROM_DEBUG:
bank.dump_bin(f"{extracted_dir}/baserom_audiotest/audiotable_files/{bank.file_name}.bin") bank.dump_bin(f"{extracted_dir}/baserom_audiotest/audiotable_files/{bank.file_name}.bin")
@ -90,7 +91,7 @@ def bank_data_lookup(sample_banks : List[Union[AudioTableFile, int]], e : Union[
return e return e
def collect_soundfonts(audiobank_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo, def collect_soundfonts(audiobank_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo,
sound_font_table : AudioCodeTable, soundfont_xmls : Dict[int, Tuple[str, Element]], sound_font_table : AudioCodeTable, soundfont_descs : Dict[int, SoundFontExtractionDescription],
sample_banks : List[Union[AudioTableFile, int]]): sample_banks : List[Union[AudioTableFile, int]]):
soundfonts = [] soundfonts = []
@ -104,7 +105,7 @@ def collect_soundfonts(audiobank_seg : memoryview, extracted_dir : str, version_
# Read the data # Read the data
soundfont = AudiobankFile(audiobank_seg, i, entry, sound_font_table.rom_addr, bank1, bank2, soundfont = AudiobankFile(audiobank_seg, i, entry, sound_font_table.rom_addr, bank1, bank2,
entry.sample_bank_id_1, entry.sample_bank_id_2, entry.sample_bank_id_1, entry.sample_bank_id_2,
extraction_xml=soundfont_xmls.get(i, None)) extraction_desc=soundfont_descs.get(i, None))
soundfonts.append(soundfont) soundfonts.append(soundfont)
if BASEROM_DEBUG: if BASEROM_DEBUG:
@ -186,7 +187,7 @@ def disassemble_one_sequence(extracted_dir : str, version_info : GameVersionInfo
def extract_sequences(audioseq_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo, write_xml : bool, def extract_sequences(audioseq_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo, write_xml : bool,
sequence_table : AudioCodeTable, sequence_font_table : memoryview, sequence_table : AudioCodeTable, sequence_font_table : memoryview,
sequence_xmls : Dict[int, Element], soundfonts : List[AudiobankFile]): sequence_descs : Dict[int, SequenceExtractionDescription], soundfonts : List[AudiobankFile]):
sequence_font_table_cvg = [0] * len(sequence_font_table) sequence_font_table_cvg = [0] * len(sequence_font_table)
@ -237,13 +238,13 @@ def extract_sequences(audioseq_seg : memoryview, extracted_dir : str, version_in
with open(f"{extracted_dir}/baserom_audiotest/audioseq_files/seq_{i}{ext}.aseq", "wb") as outfile: with open(f"{extracted_dir}/baserom_audiotest/audioseq_files/seq_{i}{ext}.aseq", "wb") as outfile:
outfile.write(seq_data) outfile.write(seq_data)
extraction_xml = sequence_xmls.get(i, None) extraction_desc = sequence_descs.get(i, None)
if extraction_xml is None: if extraction_desc is None:
sequence_filename = f"seq_{i}" sequence_filename = f"seq_{i}"
sequence_name = f"Sequence_{i}" sequence_name = f"Sequence_{i}"
else: else:
sequence_filename = extraction_xml[0] sequence_filename = extraction_desc.file_name
sequence_name = extraction_xml[1].attrib["Name"] sequence_name = extraction_desc.name
# Write extraction xml entry # Write extraction xml entry
if write_xml: if write_xml:
@ -359,27 +360,22 @@ def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : st
# Collect extraction xmls # Collect extraction xmls
# ================================================================================================================== # ==================================================================================================================
samplebank_xmls : Dict[int, Tuple[str, Element]] = {} samplebank_descs : Dict[int, SampleBankExtractionDescription] = {}
soundfont_xmls : Dict[int, Tuple[str, Element]] = {} soundfont_descs : Dict[int, SoundFontExtractionDescription] = {}
sequence_xmls : Dict[int, Tuple[str, Element]] = {} sequence_descs : Dict[int, SequenceExtractionDescription] = {}
if read_xml: if read_xml:
# Read all present xmls # Read all present xmls
def walk_xmls(out_dict : Dict[int, Tuple[str, Element]], path : str, typename : str): def walk_xmls(T : type, out_dict : Dict[int, ExtractionDescription], path : str):
for root,_,files in os.walk(path): for root,_,files in os.walk(path):
for f in files: for f in files:
fullpath = os.path.join(root, f) desc : ExtractionDescription = T(os.path.join(root, f), f, version_info.version_name)
xml = ElementTree.parse(fullpath) out_dict[desc.index] = desc
xml_root = xml.getroot()
if xml_root.tag != typename or "Name" not in xml_root.attrib or "Index" not in xml_root.attrib: walk_xmls(SampleBankExtractionDescription, samplebank_descs, f"assets/xml/audio/samplebanks")
error(f"Malformed {typename} extraction xml: \"{fullpath}\"") walk_xmls(SoundFontExtractionDescription, soundfont_descs, f"assets/xml/audio/soundfonts")
out_dict[int(xml_root.attrib["Index"])] = (f.replace(".xml", ""), xml_root) walk_xmls(SequenceExtractionDescription, sequence_descs, f"assets/xml/audio/sequences")
walk_xmls(samplebank_xmls, f"assets/xml/audio/samplebanks", "SampleBank")
walk_xmls(soundfont_xmls, f"assets/xml/audio/soundfonts", "SoundFont")
walk_xmls(sequence_xmls, f"assets/xml/audio/sequences", "Sequence")
# TODO warn about any missing xmls or xmls with a bad index # TODO warn about any missing xmls or xmls with a bad index
@ -389,7 +385,7 @@ def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : st
if BASEROM_DEBUG: if BASEROM_DEBUG:
os.makedirs(f"{extracted_dir}/baserom_audiotest/audiotable_files", exist_ok=True) os.makedirs(f"{extracted_dir}/baserom_audiotest/audiotable_files", exist_ok=True)
sample_banks = collect_sample_banks(audiotable_seg, extracted_dir, version_info, sample_bank_table, samplebank_xmls) sample_banks = collect_sample_banks(audiotable_seg, extracted_dir, version_info, sample_bank_table, samplebank_descs)
# ================================================================================================================== # ==================================================================================================================
# Collect soundfonts # Collect soundfonts
@ -397,7 +393,7 @@ def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : st
if BASEROM_DEBUG: if BASEROM_DEBUG:
os.makedirs(f"{extracted_dir}/baserom_audiotest/audiobank_files", exist_ok=True) os.makedirs(f"{extracted_dir}/baserom_audiotest/audiobank_files", exist_ok=True)
soundfonts = collect_soundfonts(audiobank_seg, extracted_dir, version_info, sound_font_table, soundfont_xmls, soundfonts = collect_soundfonts(audiobank_seg, extracted_dir, version_info, sound_font_table, soundfont_descs,
sample_banks) sample_banks)
# ================================================================================================================== # ==================================================================================================================
@ -459,4 +455,4 @@ def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : st
print("Extracting sequences...") print("Extracting sequences...")
extract_sequences(audioseq_seg, extracted_dir, version_info, write_xml, sequence_table, sequence_font_table, extract_sequences(audioseq_seg, extracted_dir, version_info, write_xml, sequence_table, sequence_font_table,
sequence_xmls, soundfonts) sequence_descs, soundfonts)

View File

@ -5,13 +5,13 @@
# #
import struct import struct
from typing import Optional, Tuple from typing import Optional
from xml.etree.ElementTree import Element
from .audio_tables import AudioCodeTableEntry from .audio_tables import AudioCodeTableEntry
from .audiobank_structs import AdpcmBook, AdpcmLoop, Drum, Instrument, SoundFontSample, SoundFontSound from .audiobank_structs import AdpcmBook, AdpcmLoop, Drum, Instrument, SoundFontSample, SoundFontSound
from .envelope import Envelope
from .audiotable import AudioTableFile, AudioTableSample from .audiotable import AudioTableFile, AudioTableSample
from .envelope import Envelope
from .extraction_xml import SoundFontExtractionDescription
from .tuning import pitch_names from .tuning import pitch_names
from .util import XMLWriter, align, debugm, merge_like_ranges, merge_ranges from .util import XMLWriter, align, debugm, merge_like_ranges, merge_ranges
@ -183,7 +183,7 @@ class AudiobankFile:
def __init__(self, audiobank_seg : memoryview, index : int, table_entry : AudioCodeTableEntry, def __init__(self, audiobank_seg : memoryview, index : int, table_entry : AudioCodeTableEntry,
seg_offset : int, bank1 : AudioTableFile, bank2 : AudioTableFile, bank1_num : int, bank2_num : int, seg_offset : int, bank1 : AudioTableFile, bank2 : AudioTableFile, bank1_num : int, bank2_num : int,
extraction_xml : Tuple[str, Element] = None): extraction_desc : Optional[SoundFontExtractionDescription] = None):
self.bank_num = index self.bank_num = index
self.table_entry : AudioCodeTableEntry = table_entry self.table_entry : AudioCodeTableEntry = table_entry
self.num_instruments = self.table_entry.num_instruments self.num_instruments = self.table_entry.num_instruments
@ -193,7 +193,7 @@ class AudiobankFile:
self.bank1_num = bank1_num self.bank1_num = bank1_num
self.bank2_num = bank2_num self.bank2_num = bank2_num
if extraction_xml is None: if extraction_desc is None:
self.file_name = f"Soundfont_{self.bank_num}" self.file_name = f"Soundfont_{self.bank_num}"
self.name = f"Soundfont_{self.bank_num}" self.name = f"Soundfont_{self.bank_num}"
@ -201,32 +201,22 @@ class AudiobankFile:
self.extraction_instruments_info = None self.extraction_instruments_info = None
self.extraction_drums_info = None self.extraction_drums_info = None
self.extraction_effects_info = None self.extraction_effects_info = None
self.extraction_envelopes_info_versions = []
self.extraction_instruments_info_versions = {}
self.extraction_drums_info_versions = []
self.extraction_effects_info_versions = []
else: else:
self.file_name = extraction_xml[0] self.file_name = extraction_desc.file_name
self.name = extraction_xml[1].attrib["Name"] self.name = extraction_desc.name
self.extraction_envelopes_info = [] self.extraction_envelopes_info = extraction_desc.envelopes_info
self.extraction_instruments_info = {} self.extraction_instruments_info = extraction_desc.instruments_info
self.extraction_drums_info = [] self.extraction_drums_info = extraction_desc.drums_info
self.extraction_effects_info = [] self.extraction_effects_info = extraction_desc.effects_info
self.extraction_envelopes_info_versions = extraction_desc.envelopes_info_versions
for item in extraction_xml[1]: self.extraction_instruments_info_versions = extraction_desc.instruments_info_versions
if item.tag == "Envelopes": self.extraction_drums_info_versions = extraction_desc.drums_info_versions
for env in item: self.extraction_effects_info_versions = extraction_desc.effects_info_versions
assert env.tag == "Envelope"
self.extraction_envelopes_info.append(env.attrib["Name"])
elif item.tag == "Instruments":
for instr in item:
assert instr.tag == "Instrument"
self.extraction_instruments_info[int(instr.attrib["ProgramNumber"])] = instr.attrib["Name"]
elif item.tag == "Drums":
for drum in item:
self.extraction_drums_info.append(drum.attrib["Name"])
elif item.tag == "Effects":
for effect in item:
self.extraction_effects_info.append(effect.attrib["Name"])
else:
assert False, item.tag
# Coverage consists of a list of itervals of the form [[start,type],[end,type]] # Coverage consists of a list of itervals of the form [[start,type],[end,type]]
self.coverage = [] self.coverage = []
@ -755,25 +745,25 @@ class AudiobankFile:
# TODO resolve decay/release index overrides? # TODO resolve decay/release index overrides?
def envelope_name(self, index): def envelope_name(self, index):
if self.extraction_envelopes_info is not None: if self.extraction_envelopes_info is not None and index < len(self.extraction_envelopes_info):
return self.extraction_envelopes_info[index] return self.extraction_envelopes_info[index]
else: else:
return f"Env{index}" return f"Env{index}"
def instrument_name(self, program_number): def instrument_name(self, program_number):
if self.extraction_instruments_info is not None: if self.extraction_instruments_info is not None and program_number in self.extraction_instruments_info:
return self.extraction_instruments_info[program_number] return self.extraction_instruments_info[program_number]
else: else:
return f"INST_{program_number}" return f"INST_{program_number}"
def drum_grp_name(self, index): def drum_grp_name(self, index):
if self.extraction_drums_info is not None: if self.extraction_drums_info is not None and index < len(self.extraction_drums_info):
return self.extraction_drums_info[index] return self.extraction_drums_info[index]
else: else:
return f"DRUM_{index}" return f"DRUM_{index}"
def effect_name(self, index): def effect_name(self, index):
if self.extraction_effects_info is not None: if self.extraction_effects_info is not None and index < len(self.extraction_effects_info):
return self.extraction_effects_info[index] return self.extraction_effects_info[index]
else: else:
return f"EFFECT_{index}" return f"EFFECT_{index}"
@ -905,21 +895,41 @@ class AudiobankFile:
# add contents for names # add contents for names
if len(self.envelopes) != 0: if len(self.envelopes) != 0 or len(self.extraction_envelopes_info_versions) != 0:
xml.write_start_tag("Envelopes") xml.write_start_tag("Envelopes")
for i in range(len(self.envelopes)): # First write envelopes that were defined in the extraction xml, possibly interleaved with envelopes
# we ignored for this version
i = 0
for envelope_entry,in_version in self.extraction_envelopes_info_versions:
xml.write_element("Envelope", envelope_entry)
# Count how many envelopes we saw that were defined for this version
i += in_version
# Write any remaining envelopes that weren't defined in the xml
for j in range(i, len(self.envelopes)):
xml.write_element("Envelope", { xml.write_element("Envelope", {
"Name" : self.envelope_name(i) "Name" : self.envelope_name(j)
}) })
xml.write_end_tag() xml.write_end_tag()
if len(self.instruments) != 0: if len(self.instruments) != 0 or len(self.extraction_instruments_info_versions) != 0:
xml.write_start_tag("Instruments") xml.write_start_tag("Instruments")
# Write in struct order # Write in struct order
for instr in sorted(self.instruments, key=lambda instr : instr.struct_index): sorted_instruments = tuple(sorted(self.instruments, key=lambda instr : instr.struct_index))
# First write instruments that were defined in the extraction xml, possibly interleaved with instruments
# we ignored for this version
i = 0
for instr_entry,in_version in self.extraction_instruments_info_versions:
xml.write_element("Instrument", instr_entry)
# Count how many instruments we saw that were defined for this version
i += in_version
# Write any remaining instruments that weren't defined in the xml
for instr in sorted_instruments[i:]:
instr : Instrument instr : Instrument
if not instr.unused: if not instr.unused:
xml.write_element("Instrument", { xml.write_element("Instrument", {
@ -929,23 +939,39 @@ class AudiobankFile:
xml.write_end_tag() xml.write_end_tag()
if any(isinstance(dg, DrumGroup) for dg in self.drum_groups): if any(isinstance(dg, DrumGroup) for dg in self.drum_groups) or len(self.extraction_drums_info_versions):
xml.write_start_tag("Drums") xml.write_start_tag("Drums")
for i,drum_grp in enumerate(self.drum_groups): # First write drums that were defined in the extraction xml, possibly interleaved with drums
# we ignored for this version
i = 0
for drum_entry,in_version in self.extraction_drums_info_versions:
xml.write_element("Drum", drum_entry)
# Count how many drum groups we saw that were defined for this version
i += in_version
for j,drum_grp in enumerate(self.drum_groups[i:], i):
if isinstance(drum_grp, DrumGroup): if isinstance(drum_grp, DrumGroup):
xml.write_element("Drum", { xml.write_element("Drum", {
"Name" : self.drum_grp_name(i) "Name" : self.drum_grp_name(j)
}) })
xml.write_end_tag() xml.write_end_tag()
if len(self.sfx) != 0: if len(self.sfx) != 0 or len(self.extraction_effects_info_versions):
xml.write_start_tag("Effects") xml.write_start_tag("Effects")
for i,sfx in enumerate(self.sfx): # First write effects that were defined in the extraction xml, possibly interleaved with effects
# we ignored for this version
i = 0
for sfx_entry,in_version in self.extraction_effects_info_versions:
xml.write_element("Effect", sfx_entry)
# Count how many effects we saw that were defined for this version
i += in_version
for j,sfx in enumerate(self.sfx[i:], i):
xml.write_element("Effect", { xml.write_element("Effect", {
"Name" : self.effect_name(i) "Name" : self.effect_name(j)
}) })
xml.write_end_tag() xml.write_end_tag()

View File

@ -5,11 +5,11 @@
# #
import math, struct import math, struct
from typing import Dict, Tuple from typing import Dict, Optional
from xml.etree.ElementTree import Element
from .audio_tables import AudioCodeTableEntry from .audio_tables import AudioCodeTableEntry
from .audiobank_structs import AudioSampleCodec, SoundFontSample, AdpcmBook, AdpcmLoop from .audiobank_structs import AudioSampleCodec, SoundFontSample, AdpcmBook, AdpcmLoop
from .extraction_xml import SampleBankExtractionDescription
from .tuning import pitch_names, note_z64_to_midi, recalc_tuning, rate_from_tuning, rank_rates_notes, BAD_FLOATS from .tuning import pitch_names, note_z64_to_midi, recalc_tuning, rate_from_tuning, rank_rates_notes, BAD_FLOATS
from .util import align, error, XMLWriter, f32_to_u32 from .util import align, error, XMLWriter, f32_to_u32
@ -207,7 +207,7 @@ class AudioTableSample(AudioTableData):
def base_note_number(self): def base_note_number(self):
return note_z64_to_midi(pitch_names.index(self.base_note)) return note_z64_to_midi(pitch_names.index(self.base_note))
def resolve_basenote_rate(self, extraction_sample_info : Dict[int, Dict[str,str]]): def resolve_basenote_rate(self, extraction_sample_info : Optional[Dict[str,str]]):
assert len(self.notes_rates) != 0 assert len(self.notes_rates) != 0
# rate_3ds = None # rate_3ds = None
@ -285,13 +285,9 @@ class AudioTableSample(AudioTableData):
final_rate,(final_note,) = rank_rates_notes(finalists) final_rate,(final_note,) = rank_rates_notes(finalists)
if extraction_sample_info is not None: if extraction_sample_info is not None:
if self.start in extraction_sample_info: assert "SampleRate" in extraction_sample_info and "BaseNote" in extraction_sample_info
entry = extraction_sample_info[self.start] final_rate = int(extraction_sample_info["SampleRate"])
if "SampleRate" in entry and "BaseNote" in entry: final_note = extraction_sample_info["BaseNote"]
final_rate = int(entry["SampleRate"])
final_note = entry["BaseNote"]
else:
print(f"WARNING: Missing extraction xml entry for sample at offset=0x{self.start:X}")
# print(" ",len(FINAL_NOTES_RATES), FINAL_NOTES_RATES) # print(" ",len(FINAL_NOTES_RATES), FINAL_NOTES_RATES)
# if rate_3ds is not None and len(FINAL_NOTES_RATES) == 1: # if rate_3ds is not None and len(FINAL_NOTES_RATES) == 1:
@ -385,7 +381,8 @@ class AudioTableFile:
""" """
def __init__(self, bank_num : int, audiotable_seg : memoryview, table_entry : AudioCodeTableEntry, def __init__(self, bank_num : int, audiotable_seg : memoryview, table_entry : AudioCodeTableEntry,
seg_offset : int, buffer_bug : bool = False, extraction_xml : Tuple[str, Element] = None): seg_offset : int, buffer_bug : bool = False,
extraction_desc : Optional[SampleBankExtractionDescription] = None):
self.bank_num = bank_num self.bank_num = bank_num
self.table_entry : AudioCodeTableEntry = table_entry self.table_entry : AudioCodeTableEntry = table_entry
self.data = self.table_entry.data(audiotable_seg, seg_offset) self.data = self.table_entry.data(audiotable_seg, seg_offset)
@ -393,24 +390,18 @@ class AudioTableFile:
self.samples_final = None self.samples_final = None
if extraction_xml is None: if extraction_desc is None:
self.file_name = f"SampleBank_{self.bank_num}" self.file_name = f"SampleBank_{self.bank_num}"
self.name = f"SampleBank_{self.bank_num}" self.name = f"SampleBank_{self.bank_num}"
self.extraction_sample_info_versions = []
self.extraction_sample_info = None self.extraction_sample_info = None
self.extraction_blob_info = None self.extraction_blob_info = None
else: else:
self.file_name = extraction_xml[0] self.file_name = extraction_desc.file_name
self.name = extraction_xml[1].attrib["Name"] self.name = extraction_desc.name
self.extraction_sample_info_versions = extraction_desc.sample_info_versions
self.extraction_sample_info = {} self.extraction_sample_info = extraction_desc.sample_info
self.extraction_blob_info = {} self.extraction_blob_info = extraction_desc.blob_info
for item in extraction_xml[1]:
if item.tag == "Sample":
self.extraction_sample_info[int(item.attrib["Offset"], 16)] = item.attrib
elif item.tag == "Blob":
self.extraction_blob_info[int(item.attrib["Offset"], 16)] = item.attrib
else:
assert False
self.pointer_indices = [] self.pointer_indices = []
@ -461,28 +452,24 @@ class AudioTableFile:
return self.samples[offset] return self.samples[offset]
def sample_name(self, sample : AudioTableSample, index : int): def sample_name(self, sample : AudioTableSample, index : int):
if self.extraction_sample_info is not None: if self.extraction_sample_info is not None and index < len(self.extraction_sample_info):
if sample.start in self.extraction_sample_info: return self.extraction_sample_info[index]["Name"]
return self.extraction_sample_info[sample.start]["Name"]
print(f"WARNING: Missing extraction xml entry for sample at offset=0x{sample.start:X}")
return f"SAMPLE_{self.bank_num}_{index}" return f"SAMPLE_{self.bank_num}_{index}"
def sample_filename(self, sample : AudioTableSample, index : int): def sample_filename(self, sample : AudioTableSample, index : int):
ext = sample.codec_file_extension_compressed() ext = sample.codec_file_extension_compressed()
if self.extraction_sample_info is not None: if self.extraction_sample_info is not None and index < len(self.extraction_sample_info):
if sample.start in self.extraction_sample_info: return self.extraction_sample_info[index]["FileName"] + ext
return self.extraction_sample_info[sample.start]["FileName"] + ext
print(f"WARNING: Missing extraction xml entry for sample at offset=0x{sample.start:X}")
npad = int(math.floor(1 + math.log10(len(self.samples)))) if len(self.samples) != 0 else 0 npad = int(math.floor(1 + math.log10(len(self.samples)))) if len(self.samples) != 0 else 0
return f"Sample{index:0{npad}}{ext}" return f"Sample{index:0{npad}}{ext}"
def blob_filename(self, start, end): def blob_filename(self, start, end, index):
if self.extraction_blob_info is not None: if self.extraction_blob_info is not None and index < len(self.extraction_blob_info):
if start in self.extraction_blob_info: return self.extraction_blob_info[index]["Name"]
return self.extraction_blob_info[start]["Name"]
print(f"WARNING: Missing extraction xml entry for blob at offset=0x{start:X}")
return f"UNACCOUNTED_{start:X}_{end:X}" return f"UNACCOUNTED_{start:X}_{end:X}"
def finalize_samples(self): def finalize_samples(self):
@ -490,7 +477,7 @@ class AudioTableFile:
for i,sample in enumerate(self.samples_final): for i,sample in enumerate(self.samples_final):
sample : AudioTableSample sample : AudioTableSample
sample.resolve_basenote_rate(self.extraction_sample_info) sample.resolve_basenote_rate(self.extraction_sample_info[i] if self.extraction_sample_info is not None else None)
def finalize_coverage(self, all_sample_banks): def finalize_coverage(self, all_sample_banks):
if len(self.coverage) != 0: if len(self.coverage) != 0:
@ -577,6 +564,7 @@ class AudioTableFile:
def assign_names(self): def assign_names(self):
i = 0 i = 0
j = 0
for sample in self.samples_final: for sample in self.samples_final:
if isinstance(sample, AudioTableSample): if isinstance(sample, AudioTableSample):
sample : AudioTableSample sample : AudioTableSample
@ -587,9 +575,10 @@ class AudioTableFile:
else: else:
sample : AudioTableData sample : AudioTableData
name = self.blob_filename(sample.start, sample.end) name = self.blob_filename(sample.start, sample.end, j)
sample.name = name sample.name = name
sample.filename = f"{name}.bin" sample.filename = f"{name}.bin"
j += 1
def to_xml(self, base_path): def to_xml(self, base_path):
xml = XMLWriter() xml = XMLWriter()
@ -635,33 +624,36 @@ class AudioTableFile:
xml.write_comment("This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/") xml.write_comment("This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/")
start = { xml.write_start_tag("SampleBank", {
"Name" : self.name, "Name" : self.name,
"Index" : self.bank_num, "Index" : self.bank_num,
} })
xml.write_start_tag("SampleBank", start)
# Write elements from the old xml version verbatim
i = 0 i = 0
for sample in self.samples_final: for entry_name,entry_attrs,in_version in self.extraction_sample_info_versions:
xml.write_element(entry_name, entry_attrs)
i += in_version
# Write any new elements
for sample in self.samples_final[i:]:
if isinstance(sample, AudioTableSample): if isinstance(sample, AudioTableSample):
sample : AudioTableSample sample : AudioTableSample
xml.write_element("Sample", { attrs = {
"Name" : sample.name, "Name" : sample.name,
"FileName" : sample.filename.replace(sample.codec_file_extension_compressed(), ""), "FileName" : sample.filename.replace(sample.codec_file_extension_compressed(), ""),
"Offset" : f"0x{sample.start:06X}",
"SampleRate" : sample.sample_rate, "SampleRate" : sample.sample_rate,
"BaseNote" : sample.base_note, "BaseNote" : sample.base_note,
}) }
i += 1 xml.write_element("Sample", attrs)
else: else:
sample : AudioTableData sample : AudioTableData
xml.write_element("Blob", { attrs = {
"Name" : sample.name, "Name" : sample.name,
"Offset" : f"0x{sample.start:06X}", }
"Size" : f"0x{sample.end - sample.start:X}", xml.write_element("Blob", attrs)
})
xml.write_end_tag() xml.write_end_tag()

View File

@ -51,17 +51,7 @@ from enum import Enum, auto
from typing import Callable, Dict, List, Optional, Tuple from typing import Callable, Dict, List, Optional, Tuple
from .audiobank_file import AudiobankFile from .audiobank_file import AudiobankFile
from .tuning import pitch_names
pitch_names = (
"A0", "BF0", "B0", "C1", "DF1", "D1", "EF1", "E1", "F1", "GF1", "G1", "AF1", "A1", "BF1", "B1", "C2",
"DF2", "D2", "EF2", "E2", "F2", "GF2", "G2", "AF2", "A2", "BF2", "B2", "C3", "DF3", "D3", "EF3", "E3",
"F3", "GF3", "G3", "AF3", "A3", "BF3", "B3", "C4", "DF4", "D4", "EF4", "E4", "F4", "GF4", "G4", "AF4",
"A4", "BF4", "B4", "C5", "DF5", "D5", "EF5", "E5", "F5", "GF5", "G5", "AF5", "A5", "BF5", "B5", "C6",
"DF6", "D6", "EF6", "E6", "F6", "GF6", "G6", "AF6", "A6", "BF6", "B6", "C7", "DF7", "D7", "EF7", "E7",
"F7", "GF7", "G7", "AF7", "A7", "BF7", "B7", "C8", "DF8", "D8", "EF8", "E8", "F8", "GF8", "G8", "AF8",
"A8", "BF8", "B8", "C9", "DF9", "D9", "EF9", "E9", "F9", "GF9", "G9", "AF9", "A9", "BF9", "B9", "C10",
"DF10", "D10", "EF10", "E10", "F10", "BFNEG1", "BNEG1", "C0", "DF0", "D0", "EF0", "E0", "F0", "GF0", "G0", "AF0",
)
# #
# VERSIONS # VERSIONS
@ -1277,20 +1267,33 @@ class SequenceDisassembler:
outfile.write(f".endseq {self.seq_name}\n") outfile.write(f".endseq {self.seq_name}\n")
if __name__ == '__main__': if __name__ == '__main__':
import sys import argparse
parser = argparse.ArgumentParser(description="Disassemble a Zelda 64 sequence binary")
parser.add_argument("file", help="Sequence binary to disassemble")
parser.add_argument("out", help="Path to output source file")
parser.add_argument("-v", dest="mml_version", required=False, default="OoT", type=str, help="Sample rate (integer)")
args = parser.parse_args()
in_path = sys.argv[1] in_path = args.file
out_path = sys.argv[2] out_path = args.out
mml_ver = {
"OoT" : MMLVersion.OOT,
"MM" : MMLVersion.MM,
}.get(args.mml_version, None)
if mml_ver is None:
raise Exception("Invalid MML Version, should be 'OoT' or 'MM'")
with open(in_path, "rb") as infile: with open(in_path, "rb") as infile:
data = bytearray(infile.read()) data = bytearray(infile.read())
class FontDummy: class FontDummy:
def __init__(self, file_name) -> None: def __init__(self, name) -> None:
self.name = file_name self.name = name
self.file_name = file_name self.file_name = name
self.instrument_index_map = {} self.instrument_index_map = {}
disas = SequenceDisassembler(0, data, None, CMD_SPEC, MMLVersion.MM, out_path, "", [FontDummy("wow")], []) disas = SequenceDisassembler(0, data, None, CMD_SPEC, mml_ver, out_path, "", [FontDummy("dummyfont")], [])
disas.analyze() disas.analyze()
disas.emit() disas.emit()

View File

@ -0,0 +1,135 @@
# SPDX-FileCopyrightText: © 2024 ZeldaRET
# SPDX-License-Identifier: CC0-1.0
#
#
#
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from .util import error
class ExtractionDescription:
def __init__(self, file_path : str, file_name : str, version_name : str) -> None:
self.type_name = type(self).__name__.replace("ExtractionDescription", "")
self.file_name = file_name.replace(".xml", "")
self.file_path = file_path
xml_root = ElementTree.parse(file_path).getroot()
if xml_root.tag != self.type_name or "Name" not in xml_root.attrib or "Index" not in xml_root.attrib:
error(f"Malformed {self.type_name} extraction xml: \"{file_path}\"")
self.name = xml_root.attrib["Name"]
self.index = int(xml_root.attrib["Index"])
self.post_init(xml_root, version_name)
def post_init(self, xml_root : Element, version_name : str):
raise NotImplementedError() # Implement in subclass
def in_version(self, version_include, version_exclude, version_name : str):
if version_include == "":
version_include = "All"
if version_exclude == "":
version_exclude = "None"
# Determine if this layout is the one we need
if version_include != "All":
version_include = version_include.split(",")
if version_exclude != "None":
version_exclude = version_exclude.split(",")
included = version_include == "All" or version_name in version_include
excluded = version_exclude != "None" and version_name in version_exclude
return included and not excluded
class SampleBankExtractionDescription(ExtractionDescription):
def post_init(self, xml_root : Element, version_name : str):
self.included_version = None
self.sample_info = []
self.sample_info_versions = []
self.blob_info = []
for item in xml_root:
if item.tag == "Sample":
version_include = item.attrib.get("VersionInclude", "")
version_exclude = item.attrib.get("VersionExclude", "")
in_version = self.in_version(version_include, version_exclude, version_name)
if in_version:
self.sample_info.append(item.attrib)
self.sample_info_versions.append((item.tag, item.attrib, in_version))
elif item.tag == "Blob":
version_include = item.attrib.get("VersionInclude", "")
version_exclude = item.attrib.get("VersionExclude", "")
in_version = self.in_version(version_include, version_exclude, version_name)
if in_version:
self.blob_info.append(item.attrib)
self.sample_info_versions.append((item.tag, item.attrib, in_version))
else:
print(xml_root.attrib)
assert False, item.tag
class SoundFontExtractionDescription(ExtractionDescription):
def post_init(self, xml_root : Element, version_name : str):
self.envelopes_info = []
self.instruments_info = {}
self.drums_info = []
self.effects_info = []
self.envelopes_info_versions = []
self.instruments_info_versions = []
self.drums_info_versions = []
self.effects_info_versions = []
for item in xml_root:
if item.tag == "Envelopes":
for env in item:
assert env.tag == "Envelope"
version_include = env.attrib.get("VersionInclude", "")
version_exclude = env.attrib.get("VersionExclude", "")
in_version = self.in_version(version_include, version_exclude, version_name)
if in_version:
self.envelopes_info.append(env.attrib["Name"])
self.envelopes_info_versions.append((env.attrib, in_version))
elif item.tag == "Instruments":
for instr in item:
assert instr.tag == "Instrument"
prg_num = int(instr.attrib["ProgramNumber"])
version_include = instr.attrib.get("VersionInclude", "")
version_exclude = instr.attrib.get("VersionExclude", "")
in_version = self.in_version(version_include, version_exclude, version_name)
if in_version:
self.instruments_info[prg_num] = instr.attrib["Name"]
self.instruments_info_versions.append((instr.attrib, in_version))
elif item.tag == "Drums":
for drum in item:
assert drum.tag == "Drum"
version_include = drum.attrib.get("VersionInclude", "")
version_exclude = drum.attrib.get("VersionExclude", "")
in_version = self.in_version(version_include, version_exclude, version_name)
if in_version:
self.drums_info.append(drum.attrib["Name"])
self.drums_info_versions.append((drum.attrib, in_version))
elif item.tag == "Effects":
for effect in item:
assert effect.tag == "Effect"
version_include = effect.attrib.get("VersionInclude", "")
version_exclude = effect.attrib.get("VersionExclude", "")
in_version = self.in_version(version_include, version_exclude, version_name)
if in_version:
self.effects_info.append(effect.attrib["Name"])
self.effects_info_versions.append((effect.attrib, in_version))
else:
assert False, item.tag
class SequenceExtractionDescription(ExtractionDescription):
def post_init(self, xml_root : Element, version_name : str):
pass

View File

@ -126,7 +126,7 @@ main(int argc, char **argv)
sb.name, sb.name); sb.name, sb.name);
// original tool appears to have a buffer clearing bug involving a buffer sized BUG_BUF_SIZE // original tool appears to have a buffer clearing bug involving a buffer sized BUG_BUF_SIZE
match_buf_ptr = (matching) ? match_buf : NULL; match_buf_ptr = (matching && sb.buffer_bug) ? match_buf : NULL;
match_buf_pos = 0; match_buf_pos = 0;
for (size_t i = 0; i < sb.num_samples; i++) { for (size_t i = 0; i < sb.num_samples; i++) {
@ -172,13 +172,13 @@ main(int argc, char **argv)
fprintf(outf, ".incbin \"%s\", 0x%lX, 0x%lX\n", path, aifc.ssnd_offset, aifc.ssnd_size); fprintf(outf, ".incbin \"%s\", 0x%lX, 0x%lX\n", path, aifc.ssnd_offset, aifc.ssnd_size);
if (matching && sb.buffer_bug && i == sb.num_samples - 1) { if (match_buf_ptr != NULL && i == sb.num_samples - 1) {
// emplace garbage // emplace garbage
size_t end = ALIGN16(match_buf_pos);
fprintf(outf, "\n# Garbage data from buffer bug\n"); fprintf(outf, "\n# Garbage data from buffer bug\n");
size_t end = ALIGN16(match_buf_pos);
for (; match_buf_pos < end; match_buf_pos++) for (; match_buf_pos < end; match_buf_pos++)
fprintf(outf, ".byte 0x%02X\n", match_buf[match_buf_pos]); fprintf(outf, ".byte 0x%02X\n", match_buf_ptr[match_buf_pos]);
} else { } else {
fputs("\n.balign 16\n", outf); fputs("\n.balign 16\n", outf);
} }

View File

@ -1,7 +1,7 @@
CC := gcc CC := gcc
CFLAGS := -Wall -Wextra -MMD CFLAGS := -Wall -Wextra -MMD
OPTFLAGS := -Og -g3 OPTFLAGS := -O3
LDFLAGS := LDFLAGS :=
CLANG_FORMAT := clang-format-14 CLANG_FORMAT := clang-format-14

View File

@ -14,6 +14,18 @@
#include "codec.h" #include "codec.h"
/**
* Creates FIR filter matrices for each page of the prediction codebook.
* For order=2: each page contains coefficients c0..7 and d0..7, the matrix resembles:
* [ c0, d0, 1, 0, 0, 0, 0, 0, 0, 0 ]
* [ c0, d1, d0, 1, 0, 0, 0, 0, 0, 0 ]
* [ c0, d2, d1, d0, 1, 0, 0, 0, 0, 0 ]
* [ c0, d3, d2, d1, d0, 1, 0, 0, 0, 0 ]
* [ c0, d4, d3, d2, d1, d0, 1, 0, 0, 0 ]
* [ c0, d5, d4, d3, d2, d1, d0, 1, 0, 0 ]
* [ c0, d6, d5, d4, d3, d2, d1, d0, 1, 0 ]
* [ c0, d7, d6, d5, d4, d3, d2, d1, d0, 1 ]
*/
int int
expand_codebook(int16_t *book_data, int32_t ****table_out, int32_t order, int32_t npredictors) expand_codebook(int16_t *book_data, int32_t ****table_out, int32_t order, int32_t npredictors)
{ {
@ -25,19 +37,25 @@ expand_codebook(int16_t *book_data, int32_t ****table_out, int32_t order, int32_
table[i][j] = MALLOC_CHECKED_INFO((order + 8) * sizeof(int32_t), "order=%d", order); table[i][j] = MALLOC_CHECKED_INFO((order + 8) * sizeof(int32_t), "order=%d", order);
} }
// For each page, create the associated matrix
for (int32_t i = 0; i < npredictors; i++) { for (int32_t i = 0; i < npredictors; i++) {
int32_t **table_entry = table[i]; int32_t **table_entry = table[i];
// Fill the first columns of the FIR filter matrix, up to the "order" column
for (int32_t j = 0; j < order; j++) { for (int32_t j = 0; j < order; j++) {
for (int32_t k = 0; k < 8; k++) for (int32_t k = 0; k < 8; k++)
table_entry[k][j] = *(book_data++); table_entry[k][j] = *(book_data++);
} }
// For each row fill except the first in the "order" column
for (int32_t k = 1; k < 8; k++) for (int32_t k = 1; k < 8; k++)
table_entry[k][order] = table_entry[k - 1][order - 1]; table_entry[k][order] = table_entry[k - 1][order - 1];
table_entry[0][order] = 1 << 11; // Place the 1.0 in the first row of the "order" column
table_entry[0][order] = 1 << 11; // 1.0 in qs4.11 fixed point
// Fill the remaining columns as a shifted-down copy of the previous column,
// adding 0s as-needed.
for (int32_t k = 1; k < 8; k++) { for (int32_t k = 1; k < 8; k++) {
int32_t j = 0; int32_t j = 0;
@ -52,6 +70,10 @@ expand_codebook(int16_t *book_data, int32_t ****table_out, int32_t order, int32_
return 0; return 0;
} }
/**
* For each FIR filter matrix associated with a codebook page, pointed to by `table`, store the first
* "order" columns to a codebook at `book_data_out`.
*/
int int
compressed_expanded_codebook(int16_t **book_data_out, int32_t ***table, int order, int npredictors) compressed_expanded_codebook(int16_t **book_data_out, int32_t ***table, int order, int npredictors)
{ {
@ -59,9 +81,9 @@ compressed_expanded_codebook(int16_t **book_data_out, int32_t ***table, int orde
MALLOC_CHECKED_INFO(sizeof(int16_t) * 8 * order * npredictors, "order=%d, npredictors=%d", order, npredictors); MALLOC_CHECKED_INFO(sizeof(int16_t) * 8 * order * npredictors, "order=%d, npredictors=%d", order, npredictors);
int n = 0; int n = 0;
for (int32_t i = 0; i < npredictors; i++) { for (int32_t i = 0; i < npredictors; i++) { // For each matrix
for (int32_t j = 0; j < order; j++) { for (int32_t j = 0; j < order; j++) { // For each column
for (int32_t k = 0; k < 8; k++) for (int32_t k = 0; k < 8; k++) // For each row
book_data[n++] = table[i][k][j]; book_data[n++] = table[i][k][j];
} }
} }
@ -207,7 +229,8 @@ vdecodeframe(uint8_t *frame, int32_t *prescaled, int32_t *state, int32_t order,
int32_t **coef_page = coef_tbl[optimalp]; int32_t **coef_page = coef_tbl[optimalp];
// Inner product with predictor coefficients // Matrix multiplication with FIR filter matrix associated with the book page, the matrix operates on
// 8 samples simultaneously so this needs to be done twice for all 16 samples in the output frame.
for (int32_t j = 0; j < 2; j++) { for (int32_t j = 0; j < 2; j++) {
int32_t in_vec[16]; int32_t in_vec[16];

View File

@ -10,7 +10,7 @@
#include "../util.h" #include "../util.h"
#include "vadpcm.h" #include "vadpcm.h"
// Levinson-Durbin algorithm for iteratively solving for prediction coefficients // Levinson-Durbin algorithm for iteratively solving for prediction and reflection coefficients, given autocorrelation
// https://en.wikipedia.org/wiki/Levinson_recursion // https://en.wikipedia.org/wiki/Levinson_recursion
static int static int
durbin(double *acvec, int order, double *reflection_coeffs, double *prediction_coeffs, double *error) durbin(double *acvec, int order, double *reflection_coeffs, double *prediction_coeffs, double *error)
@ -36,7 +36,7 @@ durbin(double *acvec, int order, double *reflection_coeffs, double *prediction_c
reflection_coeffs[i] = prediction_coeffs[i]; reflection_coeffs[i] = prediction_coeffs[i];
if (fabs(reflection_coeffs[i]) > 1.0) { if (fabs(reflection_coeffs[i]) > 1.0) {
// incr when a predictor coefficient is > 1 (indicates numerical instability) // incr when a reflection coefficient has a magnitude > 1 (indicates numerical instability in the model)
ret++; ret++;
} }
@ -82,7 +82,7 @@ kfroma(double *in, double *out, int order)
int i, j; int i, j;
double div; double div;
double temp; double temp;
double next[(order + 1)]; double next[order + 1];
int ret = 0; int ret = 0;
out[order] = in[order]; out[order] = in[order];
@ -144,29 +144,28 @@ rfroma(double *in, int n, double *out)
} }
static double static double
model_dist(double *predictors, double *data, int order) model_dist(double *mean_predictors, double *frame_predictors, int order)
{ {
double autocorrelation_data[order + 1]; double autocorrelation_frame_predictors[order + 1];
double autocorrelation_predictors[order + 1]; double autocorrelation_mean_predictors[order + 1];
double ret; double ret;
int i, j; int i, j;
// autocorrelation from data // autocorrelation from frame predictors
rfroma(data, order, autocorrelation_data); rfroma(frame_predictors, order, autocorrelation_frame_predictors);
// autocorrelation from predictors // autocorrelation from current mean predictors (?)
for (i = 0; i <= order; i++) { for (i = 0; i <= order; i++) {
autocorrelation_predictors[i] = 0.0; autocorrelation_mean_predictors[i] = 0.0;
for (j = 0; j <= order - i; j++) { for (j = 0; j <= order - i; j++)
autocorrelation_predictors[i] += predictors[j] * predictors[i + j]; autocorrelation_mean_predictors[i] += mean_predictors[j] * mean_predictors[i + j];
}
} }
// compute "model distance" (scaled L2 norm: 2 * inner(ac1, ac2) ) // compute "model distance" (scaled L2 norm: 2 * inner(ac1, ac2) )
ret = autocorrelation_data[0] * autocorrelation_predictors[0]; // this compares how good the mean predictors are to the optimal predictors for this frame
for (i = 1; i <= order; i++) { ret = autocorrelation_frame_predictors[0] * autocorrelation_mean_predictors[0];
ret += 2 * autocorrelation_data[i] * autocorrelation_predictors[i]; for (i = 1; i <= order; i++)
} ret += 2 * autocorrelation_frame_predictors[i] * autocorrelation_mean_predictors[i];
return ret; return ret;
} }
@ -363,7 +362,8 @@ split(double **predictors, double *delta, int order, int npredictors, double sca
} }
static void static void
refine(double **predictors, int order, int npredictors, double *data, int data_size, int refine_iters) refine(double **predictors, int order, int npredictors, double *all_frame_predictors, int num_frame_predictors,
int refine_iters)
{ {
int iter; int iter;
double dist; double dist;
@ -376,50 +376,55 @@ refine(double **predictors, int order, int npredictors, double *data, int data_s
int counts[npredictors]; int counts[npredictors];
double vec[order + 1]; double vec[order + 1];
// For some number of refinement iterations
for (iter = 0; iter < refine_iters; iter++) { for (iter = 0; iter < refine_iters; iter++) {
// For some number of refinement iterations // Initialize average autocorrelations
// Initialize averages
memset(counts, 0, npredictors * sizeof(int)); memset(counts, 0, npredictors * sizeof(int));
memset(rsums, 0, npredictors * (order + 1) * sizeof(double)); memset(rsums, 0, npredictors * (order + 1) * sizeof(double));
// Sum autocorrelations // Sum autocorrelations for averaging for each frame, binning them based on best fitting predictor set
for (i = 0; i < data_size; i++) { for (i = 0; i < num_frame_predictors; i++) {
best_value = 1e30; best_value = 1e30;
best_index = 0; best_index = 0;
// Find index that minimizes the "model distance" // Find the choice of predictor that minimizes the "model distance" for this frame
for (j = 0; j < npredictors; j++) { for (j = 0; j < npredictors; j++) {
dist = model_dist(predictors[j], &data[(order + 1) * i], order); // Compare with current mean predictors, the distance metric is based on autocorrelations
dist = model_dist(predictors[j], &all_frame_predictors[(order + 1) * i], order);
if (dist < best_value) { if (dist < best_value) {
// Record the new best predictors
best_value = dist; best_value = dist;
best_index = j; best_index = j;
} }
} }
counts[best_index]++; // Compute autocorrelation from optimal predictor
rfroma(&data[(order + 1) * i], order, vec); // compute autocorrelation from predictors rfroma(&all_frame_predictors[(order + 1) * i], order, vec);
// Add to average autocorrelation for the best predictor choice
for (j = 0; j <= order; j++) for (j = 0; j <= order; j++)
rsums[best_index][j] += vec[j]; // add to average autocorrelation rsums[best_index][j] += vec[j];
// Update the counter of how many frames we've summed for this predictor
counts[best_index]++;
} }
// finalize average autocorrelations // Finalize average autocorrelations
for (i = 0; i < npredictors; i++) { for (i = 0; i < npredictors; i++) {
if (counts[i] > 0) { if (counts[i] > 1) {
// Need to divide by the number of frames we summed
for (j = 0; j <= order; j++) { for (j = 0; j <= order; j++) {
rsums[i][j] /= counts[i]; rsums[i][j] /= counts[i];
} }
} }
} }
// Update the predictors with the new average autocorrelations in each bin
for (i = 0; i < npredictors; i++) { for (i = 0; i < npredictors; i++) {
// compute predictors from average autocorrelation // Compute predictors and reflection coefficients from average autocorrelation
durbin(rsums[i], order, vec, predictors[i], &dummy); durbin(rsums[i], order, vec, predictors[i], &dummy);
// vec is reflection coeffs
// clamp reflection coeffs // Clamp reflection coeffs for stability
for (j = 1; j <= order; j++) { for (j = 1; j <= order; j++) {
if (vec[j] >= 1.0) if (vec[j] >= 1.0)
vec[j] = 0.9999999999; vec[j] = 0.9999999999;
@ -427,44 +432,73 @@ refine(double **predictors, int order, int npredictors, double *data, int data_s
vec[j] = -0.9999999999; vec[j] = -0.9999999999;
} }
// clamped reflection coeffs -> predictors // Convert clamped reflection coeffs to stable predictors
afromk(vec, predictors[i], order); afromk(vec, predictors[i], order);
} }
} }
} }
static int static int
read_row(int16_t *p, double *row, int order) read_row(int16_t *out, double *predictors, int order)
{ {
double fval;
int ival;
int i, j, k; int i, j, k;
int overflows; int overflows;
double table[8][order]; double table[8][order];
// (discussion is for order=2)
//
// Converts 2 predictors a,b into the coefficients for an FIR filter matrix
// [ c0, d0, 1, 0, 0, 0, 0, 0, 0, 0 ]
// [ c0, d1, d0, 1, 0, 0, 0, 0, 0, 0 ]
// [ c0, d2, d1, d0, 1, 0, 0, 0, 0, 0 ]
// [ c0, d3, d2, d1, d0, 1, 0, 0, 0, 0 ]
// [ c0, d4, d3, d2, d1, d0, 1, 0, 0, 0 ]
// [ c0, d5, d4, d3, d2, d1, d0, 1, 0, 0 ]
// [ c0, d6, d5, d4, d3, d2, d1, d0, 1, 0 ]
// [ c0, d7, d6, d5, d4, d3, d2, d1, d0, 1 ]
//
// Multiplication by this matrix on a vector containing the previous two samples p[-2] and p[-1] and 8 residuals
// s[i] decodes 8 samples p[i] simultaneously.
// Only c0..7 and d0..7 are actually stored in the book, the decoder arranges the rest.
//
// The coefficients are those you get by substituting decoded samples into the prediction model at each step to
// express each p[i] for i in [0, 8) as a linear combination of p[-2], p[-1] and past residuals
// p[i] = a * p[i - 2] * b * p[i - 1] + s[i]
//
// c0 = a, d0 = b
// c1 = ab, d1 = a + b^2
// c2 = a^2 + ab^2, d2 = 2ab + b^3
// ...
for (i = 0; i < order; i++) { for (i = 0; i < order; i++) {
for (j = 0; j < i; j++) for (j = 0; j < i; j++)
table[i][j] = 0.0; table[i][j] = 0.0;
for (j = i; j < order; j++) for (j = i; j < order; j++)
table[i][j] = -row[order - j + i]; table[i][j] = -predictors[order - j + i];
} }
for (i = order; i < 8; i++) {
for (i = order; i < 8; i++)
for (j = 0; j < order; j++) for (j = 0; j < order; j++)
table[i][j] = 0.0; table[i][j] = 0.0;
}
for (i = 1; i < 8; i++) for (i = 1; i < 8; i++) {
for (j = 1; j <= order; j++) for (j = 1; j <= order; j++) {
if (i - j >= 0) if (i >= j) {
for (k = 0; k < order; k++) for (k = 0; k < order; k++)
table[i][k] -= row[j] * table[i - j][k]; table[i][k] -= predictors[j] * table[i - j][k];
}
}
}
// Convert double-precision book entries into qs4.11 fixed point numbers,
// rounding away from 0 and checking for overflows
overflows = 0; overflows = 0;
for (i = 0; i < order; i++) { for (i = 0; i < order; i++) {
for (j = 0; j < 8; j++) { for (j = 0; j < 8; j++) {
fval = table[j][i] * (double)(1 << 11); int ival;
double fval = table[j][i] * (double)(1 << 11);
if (fval < 0.0) { if (fval < 0.0) {
ival = (int)(fval - 0.5); ival = (int)(fval - 0.5);
if (ival < -0x8000) if (ival < -0x8000)
@ -475,7 +509,7 @@ read_row(int16_t *p, double *row, int order)
overflows++; overflows++;
} }
*(p++) = ival; *out++ = ival;
} }
} }
@ -517,27 +551,30 @@ tabledesign_run(int16_t *order_out, int16_t *npredictors_out, int16_t **book_dat
for (int i = 0; i < num_order; i++) for (int i = 0; i < num_order; i++)
autocorrelation_matrix[i] = MALLOC_CHECKED_INFO(num_order * sizeof(double), "num_order=%d", num_order); autocorrelation_matrix[i] = MALLOC_CHECKED_INFO(num_order * sizeof(double), "num_order=%d", num_order);
// (back-)align to a multiple of the frame size
size_t nframes = num_samples - (num_samples % frame_size); size_t nframes = num_samples - (num_samples % frame_size);
double *data = double *all_frame_predictors =
MALLOC_CHECKED_INFO(nframes * num_order * sizeof(double), "nframes=%lu, num_order=%d", nframes, num_order); MALLOC_CHECKED_INFO(nframes * num_order * sizeof(double), "nframes=%lu, num_order=%d", nframes, num_order);
uint32_t data_size = 0; uint32_t num_frame_predictors = 0;
int16_t *sample = sample_data; int16_t *sample = sample_data;
// (back-)align to a multiple of the frame size
int16_t *sample_end = sample + nframes; int16_t *sample_end = sample + nframes;
memset(buffer, 0, frame_size * sizeof(int16_t)); memset(buffer, 0, frame_size * sizeof(*buffer));
// First, compute the optimal set of predictors for every complete frame in the signal, where optimal here means
// the predictors that minimize the mean-square error between the predicted signal and the true signal.
for (; sample < sample_end; sample += frame_size) { for (; sample < sample_end; sample += frame_size) {
// Copy sample data into second half of buffer, during the first iteration the first half is 0 while in // Copy sample data into second half of buffer, during the first iteration the first half is 0 while in
// later iterations the second half of the previous iteration is shifted into the first half. // later iterations the second half of the previous iteration is shifted into the first half.
memcpy(&buffer[frame_size], sample, frame_size * sizeof(int16_t)); memcpy(&buffer[frame_size], sample, frame_size * sizeof(*buffer));
// Compute autocorrelation vector of the two vectors in the buffer // Compute autocorrelation vector of the two vectors in the buffer
acvect(&buffer[frame_size], order, frame_size, vec); acvect(&buffer[frame_size], order, frame_size, vec);
// First element is the largest(?) // First element of autocorrelation has the largest magnitude
if (fabs(vec[0]) > design->thresh) { if (fabs(vec[0]) > design->thresh) {
// Over threshold // Over threshold
@ -552,15 +589,20 @@ tabledesign_run(int16_t *order_out, int16_t *npredictors_out, int16_t **book_dat
// R = autocorrelation matrix // R = autocorrelation matrix
// r = autocorrelation vector // r = autocorrelation vector
// a = linear prediction coefficients // a = linear prediction coefficients
// After this vec contains the prediction coefficients // After this vec contains the prediction coefficients that minimize the mean-square error.
lubksb(autocorrelation_matrix, order, perm, vec); lubksb(autocorrelation_matrix, order, perm, vec);
vec[0] = 1.0; vec[0] = 1.0;
// Compute reflection coefficients from prediction coefficients // Compute reflection coefficients from prediction coefficients
if (kfroma(vec, reflection_coeffs, order) == 0) { // Continue only if numerically stable if (kfroma(vec, reflection_coeffs, order) == 0) { // Continue only if numerically stable
data[data_size * num_order + 0] = 1.0; all_frame_predictors[num_frame_predictors * num_order] = 1.0;
// clamp the reflection coefficients // Clamp the reflection coefficients. Reflection coefficients are clamped rather than the
// predictors themselves as the reflection coefficients have a direct relationship with the
// stability of the model. If the reflection coefficients are outside of the interval (-1,1)
// the model is unstable as the associated transfer function will contain poles outside the
// complex unit disk. If the reflection coefficients are all inside the interval (-1,1) there
// are no poles outside of the unit disk, guaranteeing the stability of the model.
for (int i = 1; i < num_order; i++) { for (int i = 1; i < num_order; i++) {
if (reflection_coeffs[i] >= 1.0) if (reflection_coeffs[i] >= 1.0)
reflection_coeffs[i] = 0.9999999999; reflection_coeffs[i] = 0.9999999999;
@ -568,40 +610,44 @@ tabledesign_run(int16_t *order_out, int16_t *npredictors_out, int16_t **book_dat
reflection_coeffs[i] = -0.9999999999; reflection_coeffs[i] = -0.9999999999;
} }
// Compute prediction coefficients from reflection coefficients // Compute prediction coefficients from clamped reflection coefficients
afromk(reflection_coeffs, &data[data_size * num_order], order); afromk(reflection_coeffs, &all_frame_predictors[num_frame_predictors * num_order], order);
data_size++; num_frame_predictors++;
} }
} }
} }
// Move second vector to first vector // Move second vector to first vector
memcpy(&buffer[0], &buffer[frame_size], frame_size * sizeof(int16_t)); memcpy(&buffer[0], &buffer[frame_size], frame_size * sizeof(*buffer));
} }
// Now that predictors for every frame have been found, they need to be reduced to a manageable quantity
// (determined by npredictors) to build the prediction codebook that will be exported. First compute the average
// autocorrelation of the prediction models for all frames:
// Create a vector [1.0, 0.0, ..., 0.0] // Create a vector [1.0, 0.0, ..., 0.0]
vec[0] = 1.0; vec[0] = 1.0;
for (int i = 1; i < num_order; i++) for (int i = 1; i < num_order; i++)
vec[i] = 0.0; vec[i] = 0.0;
for (uint32_t i = 0; i < data_size; i++) { for (uint32_t i = 0; i < num_frame_predictors; i++) {
// Compute autocorrelation from predictors // Compute autocorrelation from predictors, equivalent to computing the autocorrelation on the signal produced
rfroma(&data[i * num_order], order, predictors[0]); // by following the prediction model exactly.
rfroma(&all_frame_predictors[i * num_order], order, predictors[0]);
for (int k = 1; k < num_order; k++) for (int k = 1; k < num_order; k++)
vec[k] += predictors[0][k]; vec[k] += predictors[0][k];
} }
for (int i = 1; i < num_order; i++) for (int i = 1; i < num_order; i++)
vec[i] /= data_size; vec[i] /= num_frame_predictors;
// vec is the average autocorrelation // vec now contains the average autocorrelation.
// Compute predictors for this average autocorrelation using the Levinson-Durbin algorithm.
// Compute predictors for average autocorrelation using Levinson-Durbin algorithm
double dummy; double dummy;
durbin(vec, order, reflection_coeffs, predictors[0], &dummy); durbin(vec, order, reflection_coeffs, predictors[0], &dummy);
// clamp results // Clamp resulting reflection coefficients to ensure stability
for (int i = 1; i < num_order; i++) { for (int i = 1; i < num_order; i++) {
if (reflection_coeffs[i] >= 1.0) if (reflection_coeffs[i] >= 1.0)
reflection_coeffs[i] = 0.9999999999; reflection_coeffs[i] = 0.9999999999;
@ -609,27 +655,37 @@ tabledesign_run(int16_t *order_out, int16_t *npredictors_out, int16_t **book_dat
reflection_coeffs[i] = -0.9999999999; reflection_coeffs[i] = -0.9999999999;
} }
// Convert clamped reflection coefficients to predictors // Convert clamped reflection coefficients to stable predictors
afromk(reflection_coeffs, predictors[0], order); afromk(reflection_coeffs, predictors[0], order);
// Split and refine predictors // Starting with the predictors obtained from the average autocorrelation, cluster the predictors for each frame
// via k-means until we have just npredictors worth of output
for (unsigned cur_bits = 0; cur_bits < design->bits; cur_bits++) { for (unsigned cur_bits = 0; cur_bits < design->bits; cur_bits++) {
double split_delta[num_order]; double split_delta[num_order];
// Prepare [0, ..., -1, 0]
for (int i = 0; i < num_order; i++) for (int i = 0; i < num_order; i++)
split_delta[i] = 0.0; split_delta[i] = 0.0;
split_delta[order - 1] = -1.0; split_delta[order - 1] = -1.0;
// Split the predictors into two halves
split(predictors, split_delta, order, 1 << cur_bits, 0.01); split(predictors, split_delta, order, 1 << cur_bits, 0.01);
refine(predictors, order, 1 << (1 + cur_bits), data, data_size, design->refine_iters);
// Update the values of each half to the means of the halves
refine(predictors, order, 1 << (1 + cur_bits), all_frame_predictors, num_frame_predictors,
design->refine_iters);
} }
int16_t *book_data = MALLOC_CHECKED_INFO((8 * order * npredictors + 2) * sizeof(int16_t), // Now we have the reduced set of predictors, write them into the book of size 8 * order * npredictors
"order=%d, npredictors=%d", order, npredictors);
int16_t *book_data =
MALLOC_CHECKED_INFO(8 * order * npredictors * sizeof(int16_t), "order=%d, npredictors=%d", order, npredictors);
*order_out = order; *order_out = order;
*npredictors_out = npredictors; *npredictors_out = npredictors;
// As a final step, we need to convert the predictors into coefficients for an FIR filter so that samples in a
// frame can be decoded in parallel taking advantage of the RSP's 8-lane SIMD instruction set.
int num_oflow = 0; int num_oflow = 0;
for (int i = 0; i < npredictors; i++) for (int i = 0; i < npredictors; i++)
num_oflow += read_row(&book_data[8 * order * i], predictors[i], order); num_oflow += read_row(&book_data[8 * order * i], predictors[i], order);
@ -641,7 +697,7 @@ tabledesign_run(int16_t *order_out, int16_t *npredictors_out, int16_t **book_dat
*book_data_out = book_data; *book_data_out = book_data;
free(buffer); free(buffer);
free(data); free(all_frame_predictors);
for (int i = 0; i < num_order; i++) for (int i = 0; i < num_order; i++)
free(autocorrelation_matrix[i]); free(autocorrelation_matrix[i]);

View File

@ -138,8 +138,12 @@ f64_to_f80(double f64, uint8_t *f80)
} f80tmp; } f80tmp;
// get f64 bits // get f64 bits
union {
uint64_t f64_bits = *(uint64_t *)&f64; double f;
uint64_t u;
} tp;
tp.f = f64;
uint64_t f64_bits = tp.u;
int f64_sgn = F64_GET_SGN(f64_bits); int f64_sgn = F64_GET_SGN(f64_bits);
int f64_exponent = F64_GET_EXP(f64_bits); int f64_exponent = F64_GET_EXP(f64_bits);
@ -197,8 +201,12 @@ f80_to_f64(double *f64, uint8_t *f80)
((uint64_t)f64_mantissa_hi << 32) | ((uint64_t)f64_mantissa_lo); ((uint64_t)f64_mantissa_hi << 32) | ((uint64_t)f64_mantissa_lo);
// write double // write double
union {
*f64 = *(double *)&f64_bits; double f;
uint64_t u;
} tp;
tp.u = f64_bits;
*f64 = tp.f;
} }
int int
@ -309,6 +317,8 @@ aiff_aifc_common_read(container_data *out, FILE *in, UNUSED bool matching, uint3
nloops += inst.sustainLoop.playMode != LOOP_PLAYMODE_NONE; nloops += inst.sustainLoop.playMode != LOOP_PLAYMODE_NONE;
nloops += inst.releaseLoop.playMode != LOOP_PLAYMODE_NONE; nloops += inst.releaseLoop.playMode != LOOP_PLAYMODE_NONE;
out->num_loops = nloops;
out->loops = NULL;
if (nloops != 0) { if (nloops != 0) {
out->loops = MALLOC_CHECKED_INFO(nloops * sizeof(container_loop), "nloops=%lu", nloops); out->loops = MALLOC_CHECKED_INFO(nloops * sizeof(container_loop), "nloops=%lu", nloops);
@ -495,11 +505,21 @@ aiff_aifc_common_read(container_data *out, FILE *in, UNUSED bool matching, uint3
if (!has_comm) if (!has_comm)
error("aiff/aifc has no COMM chunk"); error("aiff/aifc has no COMM chunk");
if (!has_inst)
error("aiff/aifc has no INST chunk");
if (!has_ssnd) if (!has_ssnd)
error("aiff/aifc has no SSND chunk"); error("aiff/aifc has no SSND chunk");
if (!has_inst) {
out->base_note = 60; // C4
out->fine_tune = 0;
out->key_low = 0;
out->key_hi = 127;
out->vel_low = 0;
out->vel_hi = 127;
out->gain = 0;
out->num_loops = 0;
out->loops = NULL;
}
if (out->data_type == SAMPLE_TYPE_PCM16) { if (out->data_type == SAMPLE_TYPE_PCM16) {
assert(out->data_size % 2 == 0); assert(out->data_size % 2 == 0);
assert(out->bit_depth == 16); assert(out->bit_depth == 16);

View File

@ -222,6 +222,7 @@ wav_read(container_data *out, const char *path, UNUSED bool matching)
smpl.sampler_data = le32toh(smpl.sampler_data); smpl.sampler_data = le32toh(smpl.sampler_data);
out->num_loops = smpl.num_sample_loops; out->num_loops = smpl.num_sample_loops;
out->loops = NULL;
if (out->num_loops != 0) { if (out->num_loops != 0) {
out->loops = out->loops =
MALLOC_CHECKED_INFO(out->num_loops * sizeof(container_loop), "num_loops=%u", out->num_loops); MALLOC_CHECKED_INFO(out->num_loops * sizeof(container_loop), "num_loops=%u", out->num_loops);
@ -362,11 +363,11 @@ wav_read(container_data *out, const char *path, UNUSED bool matching)
if (!has_inst) { if (!has_inst) {
out->base_note = 60; // C4 out->base_note = 60; // C4
out->fine_tune = 0; out->fine_tune = 0;
out->gain = 0;
out->key_low = 0; out->key_low = 0;
out->key_hi = 0; out->key_hi = 127;
out->vel_low = 0; out->vel_low = 0;
out->vel_hi = 0; out->vel_hi = 127;
out->gain = 0;
} }
if (!has_smpl) { if (!has_smpl) {

View File

@ -1591,7 +1591,7 @@ emit_h_effects(FILE *out, soundfont *sf)
NORETURN static void NORETURN static void
usage(const char *progname) usage(const char *progname)
{ {
fprintf(stderr, "Usage: %s [--matching] <filename.xml> <out.c> <out.h>\n", progname); fprintf(stderr, "Usage: %s [--matching] <filename.xml> <out.c> <out.h> <out.name>\n", progname);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
@ -1749,12 +1749,12 @@ main(int argc, char **argv)
fprintf(out_h, fprintf(out_h,
// clang-format off // clang-format off
"#ifdef _LANGUAGE_ASEQ" "\n" "#ifdef _LANGUAGE_ASEQ" "\n"
".pushsection .fonts, \"\", @note" "\n" ".pushsection .note.fonts, \"\", @note" "\n"
" .byte %d /*sf id*/" "\n" " .byte %d /*sf id*/" "\n"
".popsection" "\n" ".popsection" "\n"
"#endif" "\n" "#endif" "\n"
"\n", "\n",
// clang-format on // clang-format on
sf.info.index); sf.info.index);
@ -1779,8 +1779,11 @@ main(int argc, char **argv)
// emit name marker // emit name marker
FILE *out_name = fopen(filename_out_name, "w"); FILE *out_name = fopen(filename_out_name, "wb");
fprintf(out_name, "%s", sf.info.name); // We need to emit an explicit null terminator so that we can run objcopy --add-section to include the name
// in a .note.name section in the compiled object file. This is so that the string that ends up in the .note.name
// section is null-terminated, its length may be verified by any tools that read the name out of this section.
fprintf(out_name, "%s%c", sf.info.name, '\0');
fclose(out_name); fclose(out_name);
// emit dependency file if wanted // emit dependency file if wanted

View File

@ -1,6 +1,5 @@
/* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */ /* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */
/* SPDX-License-Identifier: CC0-1.0 */ /* SPDX-License-Identifier: CC0-1.0 */
#define _GNU_SOURCE
#include <ctype.h> #include <ctype.h>
#include <stdarg.h> #include <stdarg.h>
#include <stdbool.h> #include <stdbool.h>

View File

@ -200,7 +200,8 @@ if __name__ == '__main__':
), ),
} }
version_info = GameVersionInfo(MMLVersion.MM, version_info = GameVersionInfo(version,
MMLVersion.MM,
soundfont_table_code_offset, soundfont_table_code_offset,
seq_font_table_code_offset, seq_font_table_code_offset,
seq_table_code_offset, seq_table_code_offset,