C语言解析SF2:如何正确映射预置头与关联乐器及样本?
核心关联链梳理
SF2的预置到样本映射是多层索引链式关联,核心路径如下:Preset (phdr) → Preset Bag (pbag) → Preset Generator (pgen) → Instrument (inst) → Instrument Bag (ibag) → Instrument Generator (igen) → Sample Header (shdr)
每个环节的索引都是起始索引,需要连续遍历直到下一个条目起始索引或数组末尾,不能仅取单个索引。
分步索引追踪实现
1. 从PHDR到PBAG
每个PHDR结构体的wPresetBagNdx字段,代表该预置对应的第一个PBAG索引。遍历PBAG数组时,当前预置的所有PBAG范围是:从wPresetBagNdx开始,到下一个PHDR的wPresetBagNdx(或PBAG数组末尾)结束。
2. PBAG到PGEN,定位Instrument索引
每个PBAG的wGenNdx是该Bag对应的第一个PGEN起始索引。遍历PGEN时,找到类型为GEN_INSTRUMENT(值为0x0008)的生成器,其wAmount字段就是对应的INST索引。
3. INST到IBAG
逻辑与PHDR到PBAG一致:INST的wInstBagNdx是第一个IBAG的起始索引,遍历IBAG直到下一个INST的wInstBagNdx(或IBAG数组末尾)。
4. IBAG到IGEN,定位Sample索引
每个IBAG的wGenNdx是第一个IGEN的起始索引。遍历IGEN时,找到类型为GEN_SAMPLE_ID(值为0x0019)的生成器,其wAmount字段就是对应的SHDR索引,由此即可关联到样本数据。
C语言代码示例
首先定义符合SF2规范的结构体(注意SF2为小端字节序,读取时需做字节序转换):
#include <stdint.h> #include <stdio.h> // SF2核心结构体简化定义 typedef struct { char achPresetName[20]; uint16_t wPreset; uint16_t wBank; uint16_t wPresetBagNdx; uint32_t dwLibrary; uint32_t dwGenre; uint32_t dwMorphology; } SF2_PHDR; typedef struct { uint16_t wGenNdx; uint16_t wModNdx; } SF2_PBAG; typedef struct { uint16_t wOper; uint16_t wAmount; } SF2_PGEN; typedef struct { char achInstName[20]; uint16_t wInstBagNdx; } SF2_INST; typedef struct { uint16_t wGenNdx; uint16_t wModNdx; } SF2_IBAG; typedef struct { uint16_t wOper; uint16_t wAmount; } SF2_IGEN; typedef struct { char achSampleName[20]; uint32_t dwStart; uint32_t dwEnd; uint32_t dwStartLoop; uint32_t dwEndLoop; uint32_t dwSampleRate; uint8_t byOriginalPitch; int8_t chPitchCorrection; uint16_t wSampleLink; uint16_t sfSampleType; } SF2_SHDR; // 小端转主机字节序 uint16_t le16toh(uint16_t val) { #ifdef __BYTE_ORDER__ #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ return ((val >> 8) & 0xFF) | ((val << 8) & 0xFF00); #else return val; #endif #else return val; #endif }
然后实现预置到样本的关联函数:
// 输入:预置索引、各区块数据;输出:关联的样本信息 void map_preset_to_samples(int preset_idx, SF2_PHDR* phdrs, int phdr_count, SF2_PBAG* pbags, int pbag_count, SF2_PGEN* pgens, int pgen_count, SF2_INST* insts, int inst_count, SF2_IBAG* ibags, int ibag_count, SF2_IGEN* igens, int igen_count, SF2_SHDR* shdrs, int shdr_count) { SF2_PHDR* preset = &phdrs[preset_idx]; int start_pbag = le16toh(preset->wPresetBagNdx); int end_pbag = (preset_idx + 1 < phdr_count) ? le16toh(phdrs[preset_idx+1].wPresetBagNdx) : pbag_count; printf("预置: %s\n", preset->achPresetName); // 遍历当前预置的所有PBAG for (int i = start_pbag; i < end_pbag; i++) { SF2_PBAG* pbag = &pbags[i]; int start_pgen = le16toh(pbag->wGenNdx); int end_pgen = (i + 1 < pbag_count) ? le16toh(pbags[i+1].wGenNdx) : pgen_count; // 查找Instrument生成器 int inst_idx = -1; for (int j = start_pgen; j < end_pgen; j++) { SF2_PGEN* pgen = &pgens[j]; uint16_t oper = le16toh(pgen->wOper); if (oper == 0x0008) { // GEN_INSTRUMENT inst_idx = le16toh(pgen->wAmount); break; } } if (inst_idx == -1 || inst_idx >= inst_count) continue; SF2_INST* inst = &insts[inst_idx]; printf(" 关联乐器: %s\n", inst->achInstName); // 遍历乐器的IBAG int start_ibag = le16toh(inst->wInstBagNdx); int end_ibag = (inst_idx + 1 < inst_count) ? le16toh(insts[inst_idx+1].wInstBagNdx) : ibag_count; for (int k = start_ibag; k < end_ibag; k++) { SF2_IBAG* ibag = &ibags[k]; int start_igen = le16toh(ibag->wGenNdx); int end_igen = (k + 1 < ibag_count) ? le16toh(ibags[k+1].wGenNdx) : igen_count; // 查找Sample ID生成器 int shdr_idx = -1; for (int l = start_igen; l < end_igen; l++) { SF2_IGEN* igen = &igens[l]; uint16_t oper = le16toh(igen->wOper); if (oper == 0x0019) { // GEN_SAMPLE_ID shdr_idx = le16toh(igen->wAmount); break; } } if (shdr_idx == -1 || shdr_idx >= shdr_count) continue; SF2_SHDR* shdr = &shdrs[shdr_idx]; printf(" 关联样本: %s\n", shdr->achSampleName); } } }
常见问题排查
- 字节序错误:SF2所有多字节字段都是小端,若主机为大端(如部分嵌入式设备)必须转换字节序,否则索引会完全混乱。
- 索引范围处理错误:每个Bag对应的Generator是连续区间,不能仅取第一个Generator,必须遍历到下一个Bag的Generator起始索引。
- 生成器类型值错误:确认
GEN_INSTRUMENT(0x0008)和GEN_SAMPLE_ID(0x0019)的数值,主流SF2文件均遵循该规范。 - 忽略多Bag情况:一个预置/乐器可能对应多个Bag(如不同音区对应不同样本),必须遍历所有关联Bag,不能仅处理第一个。
内容的提问来源于stack exchange,提问作者Elric Lescop

