Task 650: .SDS File Format
Task 650: .SDS File Format
The .SDS file format refers to the MIDI Sample Dump Standard (SDS), a protocol embedded within MIDI System Exclusive (SysEx) messages for transferring digital audio sample data between devices. A .SDS file typically consists of a sequence of SysEx messages stored in raw binary form, beginning with a Dump Header message followed by one or more Data Packets containing the packed sample data. Handshaking messages (ACK, NAK, etc.) are usually omitted in file storage, as the file represents a unidirectional dump.
The format is defined in the MIDI 1.0 Detailed Specification and associated recommended practices. All messages are encapsulated in SysEx format (starting with 0xF0 and ending with 0xF7). Data bytes are 7-bit (0-127). Multi-byte values (14-bit or 21-bit) are transmitted LSB first, with each byte's high bit cleared.
Key Message Structures
- Dump Header (sub-ID 0x01): Describes the sample metadata.
- Structure:
F0 7E <dev> 01 <sl> <sh> <ee> <pl> <pm> <ph> <gl> <gm> <gh> <hl> <hm> <hh> <il> <im> <ih> <jj> F7(16 data bytes after sub-ID). - Data Packet (sub-ID 0x02): Contains packed sample data.
- Structure:
F0 7E <dev> 02 <kk> <120 data bytes> <ll> F7(122 data bytes after sub-ID). - Checksum
<ll>= XOR of 0x7E, , 0x02, , and 120 data bytes (masked to 7 bits).
Sample data packing is based on significant bits ():
- Bytes per word =
((<ee> - 1) // 7) + 1. - Points per packet =
120 // bytes_per_word(last packet padded with 0x00 bytes). - Sample values are unsigned (0 = full negative, 2^ - 1 = full positive); subtract 2^(ee-1) for signed representation.
- Bits are left-justified across the bytes per word.
Other messages (Dump Request, ACK, etc.) are not typically stored in .SDS files.
- Properties Intrinsic to the Format
These are derived from the Dump Header message and represent the core metadata. The sample data itself is the primary content but is not a "property" in the metadata sense; it is packed according to the rules above.
| Property | Description | Range/Type | Calculation/Bytes |
|---|---|---|---|
| Device ID | MIDI device ID for the dump. | 0-127 | |
| Sample Number | Unique identifier for the sample on the device. | 0-16383 | + ( << 7) |
| Significant Bits per Sample | Number of significant bits in each sample word. | 1-28 | |
| Sample Period | Time per sample in nanoseconds. | 0-2097151 | + ( << 7) + ( << 14) |
| Sample Length | Number of sample words in the dump. | 0-2097151 | + ( << 7) + ( << 14) |
| Loop Start | Word offset for sustain loop start. | 0-2097151 | + ( << 7) + ( << 14) |
| Loop End | Word offset for sustain loop end. | 0-2097151 | + ( << 7) + ( << 14) |
| Loop Type | Loop behavior. | 0 (forward), 1 (bidirectional), 127 (no loop) |
Derived properties (not stored but calculable):
- Sample Rate (Hz) = 1000000000 // Sample Period (if Period > 0).
- Bytes per Word = ((Significant Bits - 1) // 7) + 1.
- Points per Packet = 120 // Bytes per Word.
Direct Download Links for .SDS Files
- https://raw.githubusercontent.com/bsl/send-sds/master/layered_finger_snap_mono.sds (A valid MIDI SDS file containing a mono finger snap sample layer.)
- After extensive search across web sources, X, and repositories, only one publicly available direct download link for a .SDS file was identified. Additional .SDS files are rare online due to the format's age and niche use in vintage samplers; they can be generated using the code in sections 4-7 or tools like Audacity.
Ghost Blog Embedded HTML/JavaScript
The following is a self-contained HTML file with embedded JavaScript. It can be embedded in a Ghost blog post (e.g., via an HTML card). Users can drag and drop a .SDS file; it parses the Dump Header and dumps the properties to the screen as a formatted list. Sample data is not unpacked for display (to avoid large output), but packet count is noted.
Drag and Drop a .SDS File to Parse Properties
- Python Class
The class loads a .SDS file, parses the header and unpacks sample data into a list of integers (unsigned). It can modify properties/data and write a new file. Prints properties to console. Handles general packing/unpacking.
import struct
class SDSFile:
def __init__(self):
self.device_id = 0
self.sample_number = 0
self.significant_bits = 0
self.sample_period = 0
self.sample_length = 0
self.loop_start = 0
self.loop_end = 0
self.loop_type = 0
self.sample_data = [] # List of int (unsigned sample values)
def _get_bytes_per_word(self):
ee = self.significant_bits
if ee == 0: return 0
return ((ee - 1) // 7) + 1
def load(self, path):
with open(path, 'rb') as f:
data = f.read()
pos = 0
header_found = False
packet_num = 0
self.sample_data = []
while pos < len(data):
if data[pos] == 0xF0:
start = pos
pos += 1
if pos + 3 >= len(data): break
if data[pos] != 0x7E:
pos += 1
continue
pos += 1
dev = data[pos]
pos += 1
sub = data[pos]
pos += 1
message = []
while pos < len(data) and data[pos] != 0xF7:
message.append(data[pos])
pos += 1
if pos < len(data) and data[pos] == 0xF7:
pos += 1
if sub == 1 and len(message) == 16 and not header_found:
header_found = True
self.device_id = dev
self.sample_number = message[0] + (message[1] << 7)
self.significant_bits = message[2]
self.sample_period = message[3] + (message[4] << 7) + (message[5] << 14)
self.sample_length = message[6] + (message[7] << 7) + (message[8] << 14)
self.loop_start = message[9] + (message[10] << 7) + (message[11] << 14)
self.loop_end = message[12] + (message[13] << 7) + (message[14] << 14)
self.loop_type = message[15]
elif sub == 2 and len(message) == 122 and header_found:
kk = message[0]
if kk != packet_num % 128:
raise ValueError("Packet number mismatch")
data_bytes = message[1:121]
checksum = message[121]
calc_checksum = 0x7E ^ dev ^ 0x02 ^ kk
for b in data_bytes:
calc_checksum ^= b
calc_checksum &= 0x7F
if calc_checksum != checksum:
raise ValueError("Checksum error")
self.sample_data += self._unpack_packet(data_bytes)
packet_num += 1
if len(self.sample_data) != self.sample_length:
raise ValueError("Sample length mismatch")
def _unpack_packet(self, data_bytes):
bpw = self._get_bytes_per_word()
points = len(data_bytes) // bpw
samples = []
pos = 0
for _ in range(points):
value = 0
for _ in range(bpw):
value = (value << 7) | data_bytes[pos]
pos += 1
shift = bpw * 7 - self.significant_bits
value >>= shift
samples.append(value)
return samples
def save(self, path):
bpw = self._get_bytes_per_word()
points_per_packet = 120 // bpw if bpw > 0 else 0
header = bytearray([0xF0, 0x7E, self.device_id, 0x01])
header.append(self.sample_number & 0x7F)
header.append((self.sample_number >> 7) & 0x7F)
header.append(self.significant_bits & 0x7F)
header.append(self.sample_period & 0x7F)
header.append((self.sample_period >> 7) & 0x7F)
header.append((self.sample_period >> 14) & 0x7F)
header.append(self.sample_length & 0x7F)
header.append((self.sample_length >> 7) & 0x7F)
header.append((self.sample_length >> 14) & 0x7F)
header.append(self.loop_start & 0x7F)
header.append((self.loop_start >> 7) & 0x7F)
header.append((self.loop_start >> 14) & 0x7F)
header.append(self.loop_end & 0x7F)
header.append((self.loop_end >> 7) & 0x7F)
header.append((self.loop_end >> 14) & 0x7F)
header.append(self.loop_type & 0x7F)
header.append(0xF7)
packets = []
for i in range(0, len(self.sample_data), points_per_packet):
chunk = self.sample_data[i:i + points_per_packet]
packed = self._pack_packet(chunk)
kk = (i // points_per_packet) % 128
packet = bytearray([0xF0, 0x7E, self.device_id, 0x02, kk])
packet.extend(packed)
calc_checksum = 0x7E ^ self.device_id ^ 0x02 ^ kk
for b in packed:
calc_checksum ^= b
packet.append(calc_checksum & 0x7F)
packet.append(0xF7)
packets.append(packet)
with open(path, 'wb') as f:
f.write(header)
for p in packets:
f.write(p)
def _pack_packet(self, samples):
bpw = self._get_bytes_per_word()
data_bytes = bytearray(120)
pos = 0
for value in samples:
shift = bpw * 7 - self.significant_bits
shifted = value << shift
for j in range(bpw - 1, -1, -1):
data_bytes[pos + j] = (shifted >> (j * 7)) & 0x7F
pos += bpw
return data_bytes
def print_properties(self):
rate = 1000000000 // self.sample_period if self.sample_period > 0 else 0
print(f"Device ID: {self.device_id}")
print(f"Sample Number: {self.sample_number}")
print(f"Significant Bits per Sample: {self.significant_bits}")
print(f"Sample Period (ns): {self.sample_period}")
print(f"Sample Rate (Hz): {rate}")
print(f"Sample Length (words): {self.sample_length}")
print(f"Loop Start (words): {self.loop_start}")
print(f"Loop End (words): {self.loop_end}")
print(f"Loop Type: {self.loop_type}")
print(f"Sample Data Length: {len(self.sample_data)}")
# Usage example:
# sds = SDSFile()
# sds.load('example.sds')
# sds.print_properties()
# sds.sample_number = 1 # Modify
# sds.save('modified.sds')
- Java Class
Similar functionality in Java. UsesByteBufferfor parsing/packing.
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class SDSFile {
private int deviceId = 0;
private int sampleNumber = 0;
private int significantBits = 0;
private int samplePeriod = 0;
private int sampleLength = 0;
private int loopStart = 0;
private int loopEnd = 0;
private int loopType = 0;
private List<Integer> sampleData = new ArrayList<>();
private int getBytesPerWord() {
int ee = significantBits;
if (ee == 0) return 0;
return ((ee - 1) / 7) + 1;
}
public void load(String path) throws IOException {
byte[] data = Files.readAllBytes(Paths.get(path));
int pos = 0;
boolean headerFound = false;
int packetNum = 0;
sampleData.clear();
while (pos < data.length) {
if (data[pos] == (byte) 0xF0) {
pos++;
if (pos + 3 >= data.length) break;
if (data[pos++] != (byte) 0x7E) continue;
int dev = data[pos++] & 0xFF;
int sub = data[pos++] & 0xFF;
List<Byte> message = new ArrayList<>();
while (pos < data.length && data[pos] != (byte) 0xF7) {
message.add(data[pos++]);
}
if (pos < data.length && data[pos++] == (byte) 0xF7) {
if (sub == 1 && message.size() == 16 && !headerFound) {
headerFound = true;
deviceId = dev;
sampleNumber = (message.get(0) & 0xFF) + ((message.get(1) & 0xFF) << 7);
significantBits = message.get(2) & 0xFF;
samplePeriod = (message.get(3) & 0xFF) + ((message.get(4) & 0xFF) << 7) + ((message.get(5) & 0xFF) << 14);
sampleLength = (message.get(6) & 0xFF) + ((message.get(7) & 0xFF) << 7) + ((message.get(8) & 0xFF) << 14);
loopStart = (message.get(9) & 0xFF) + ((message.get(10)483 & 0xFF) << 7) + ((message.get(11) & 0xFF) << 14);
loopEnd = (message.get(12) & 0xFF) + ((message.get(13) & 0xFF) << 7) + ((message.get(14) & 0xFF) << 14);
loopType = message.get(15) & 0xFF;
} else if (sub == 2 && message.size() == 122 && headerFound) {
int kk = message.get(0) & 0xFF;
if (kk != packetNum % 128) throw new IllegalStateException("Packet mismatch");
byte[] dataBytes = new byte[120];
for (int i = 0; i < 120; i++) dataBytes[i] = message.get(i + 1);
int checksum = message.get(121) & 0xFF;
int calc = 0x7E ^ dev ^ 0x02 ^ kk;
for (byte b : dataBytes) calc ^= (b & 0xFF);
calc &= 0x7F;
if (calc != checksum) throw new IllegalStateException("Checksum error");
sampleData.addAll(unpackPacket(dataBytes));
packetNum++;
}
}
} else pos++;
}
if (sampleData.size() != sampleLength) throw new IllegalStateException("Length mismatch");
}
private List<Integer> unpackPacket(byte[] dataBytes) {
int bpw = getBytesPerWord();
int points = dataBytes.length / bpw;
List<Integer> samples = new ArrayList<>();
int pos = 0;
for (int p = 0; p < points; p++) {
int value = 0;
for (int j = 0; j < bpw; j++) {
value = (value << 7) | (dataBytes[pos++] & 0xFF);
}
int shift = bpw * 7 - significantBits;
value >>= shift;
samples.add(value);
}
return samples;
}
public void save(String path) throws IOException {
int bpw = getBytesPerWord();
int pointsPerPacket = 120 / bpw;
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Header
out.write(0xF0);
out.write(0x7E);
out.write(deviceId);
out.write(0x01);
out.write(sampleNumber & 0x7F);
out.write((sampleNumber >> 7) & 0x7F);
out.write(significantBits & 0x7F);
out.write(samplePeriod & 0x7F);
out.write((samplePeriod >> 7) & 0x7F);
out.write((samplePeriod >> 14) & 0x7F);
out.write(sampleLength & 0x7F);
out.write((sampleLength >> 7) & 0x7F);
out.write((sampleLength >> 14) & 0x7F);
out.write(loopStart & 0x7F);
out.write((loopStart >> 7) & 0x7F);
out.write((loopStart >> 14) & 0x7F);
out.write(loopEnd & 0x7F);
out.write((loopEnd >> 7) & 0x7F);
out.write((loopEnd >> 14) & 0x7F);
out.write(loopType & 0x7F);
out.write(0xF7);
// Packets
for (int i = 0; i < sampleData.size(); i += pointsPerPacket) {
List<Integer> chunk = sampleData.subList(i, Math.min(i + pointsPerPacket, sampleData.size()));
byte[] packed = packPacket(chunk);
int kk = (i / pointsPerPacket) % 128;
out.write(0xF0);
out.write(0x7E);
out.write(deviceId);
out.write(0x02);
out.write(kk);
out.write(packed);
int calc = 0x7E ^ deviceId ^ 0x02 ^ kk;
for (byte b : packed) calc ^= (b & 0xFF);
out.write(calc & 0x7F);
out.write(0xF7);
}
Files.write(Paths.get(path), out.toByteArray());
}
private byte[] packPacket(List<Integer> samples) {
int bpw = getBytesPerWord();
byte[] dataBytes = new byte[120];
int pos = 0;
for (int value : samples) {
int shift = bpw * 7 - significantBits;
int shifted = value << shift;
for (int j = bpw - 1; j >= 0; j--) {
dataBytes[pos + j] = (byte) ((shifted >> (j * 7)) & 0x7F);
}
pos += bpw;
}
return dataBytes;
}
public void printProperties() {
int rate = samplePeriod > 0 ? 1000000000 / samplePeriod : 0;
System.out.println("Device ID: " + deviceId);
System.out.println("Sample Number: " + sampleNumber);
System.out.println("Significant Bits per Sample: " + significantBits);
System.out.println("Sample Period (ns): " + samplePeriod);
System.out.println("Sample Rate (Hz): " + rate);
System.out.println("Sample Length (words): " + sampleLength);
System.out.println("Loop Start (words): " + loopStart);
System.out.println("Loop End (words): " + loopEnd);
System.out.println("Loop Type: " + loopType);
System.out.println("Sample Data Length: " + sampleData.size());
}
// Usage:
// SDSFile sds = new SDSFile();
// sds.load("example.sds");
// sds.printProperties();
// sds.sampleNumber = 1;
// sds.save("modified.sds");
}
- JavaScript Class (Node.js)
Usesfsfor file I/O. Similar parsing/packing.
const fs = require('fs').promises;
class SDSFile {
constructor() {
this.deviceId = 0;
this.sampleNumber = 0;
this.significantBits = 0;
this.samplePeriod = 0;
this.sampleLength = 0;
this.loopStart = 0;
this.loopEnd = 0;
this.loopType = 0;
this.sampleData = [];
}
getBytesPerWord() {
const ee = this.significantBits;
if (ee === 0) return 0;
return Math.floor((ee - 1) / 7) + 1;
}
async load(path) {
const buffer = await fs.readFile(path);
const data = new Uint8Array(buffer);
let pos = 0;
let headerFound = false;
let packetNum = 0;
this.sampleData = [];
while (pos < data.length) {
if (data[pos] === 0xF0) {
pos++;
if (pos + 3 >= data.length) break;
if (data[pos++] !== 0x7E) continue;
const dev = data[pos++];
const sub = data[pos++];
const message = [];
while (pos < data.length && data[pos] !== 0xF7) {
message.push(data[pos++]);
}
if (pos < data.length && data[pos++] === 0xF7) {
if (sub === 1 && message.length === 16 && !headerFound) {
headerFound = true;
this.deviceId = dev;
this.sampleNumber = message[0] + (message[1] << 7);
this.significantBits = message[2];
this.samplePeriod = message[3] + (message[4] << 7) + (message[5] << 14);
this.sampleLength = message[6] + (message[7] << 7) + (message[8] << 14);
this.loopStart = message[9] + (message[10] << 7) + (message[11] << 14);
this.loopEnd = message[12] + (message[13] << 7) + (message[14] << 14);
this.loopType = message[15];
} else if (sub === 2 && message.length === 122 && headerFound) {
const kk = message[0];
if (kk !== packetNum % 128) throw new Error('Packet mismatch');
const dataBytes = message.slice(1, 121);
const checksum = message[121];
let calc = 0x7E ^ dev ^ 0x02 ^ kk;
for (const b of dataBytes) calc ^= b;
calc &= 0x7F;
if (calc !== checksum) throw new Error('Checksum error');
this.sampleData = this.sampleData.concat(this.unpackPacket(dataBytes));
packetNum++;
}
}
} else pos++;
}
if (this.sampleData.length !== this.sampleLength) throw new Error('Length mismatch');
}
unpackPacket(dataBytes) {
const bpw = this.getBytesPerWord();
const points = Math.floor(dataBytes.length / bpw);
const samples = [];
let pos = 0;
for (let p = 0; p < points; p++) {
let value = 0;
for (let j = 0; j < bpw; j++) {
value = (value << 7) | dataBytes[pos++];
}
const shift = bpw * 7 - this.significantBits;
value >>= shift;
samples.push(value);
}
return samples;
}
async save(path) {
const bpw = this.getBytesPerWord();
const pointsPerPacket = Math.floor(120 / bpw);
let out = Buffer.alloc(0);
// Header
let header = Buffer.from([0xF0, 0x7E, this.deviceId, 0x01,
this.sampleNumber & 0x7F, (this.sampleNumber >> 7) & 0x7F,
this.significantBits & 0x7F,
this.samplePeriod & 0x7F, (this.samplePeriod >> 7) & 0x7F, (this.samplePeriod >> 14) & 0x7F,
this.sampleLength & 0x7F, (this.sampleLength >> 7) & 0x7F, (this.sampleLength >> 14) & 0x7F,
this.loopStart & 0x7F, (this.loopStart >> 7) & 0x7F, (this.loopStart >> 14) & 0x7F,
this.loopEnd & 0x7F, (this.loopEnd >> 7) & 0x7F, (this.loopEnd >> 14) & 0x7F,
this.loopType & 0x7F, 0xF7]);
out = Buffer.concat([out, header]);
// Packets
for (let i = 0; i < this.sampleData.length; i += pointsPerPacket) {
const chunk = this.sampleData.slice(i, i + pointsPerPacket);
const packed = this.packPacket(chunk);
const kk = Math.floor(i / pointsPerPacket) % 128;
let calc = 0x7E ^ this.deviceId ^ 0x02 ^ kk;
for (const b of packed) calc ^= b;
const packet = Buffer.from([0xF0, 0x7E, this.deviceId, 0x02, kk, ...packed, calc & 0x7F, 0xF7]);
out = Buffer.concat([out, packet]);
}
await fs.writeFile(path, out);
}
packPacket(samples) {
const bpw = this.getBytesPerWord();
const dataBytes = Buffer.alloc(120, 0);
let pos = 0;
for (const value of samples) {
const shift = bpw * 7 - this.significantBits;
let shifted = value << shift;
for (let j = bpw - 1; j >= 0; j--) {
dataBytes[pos + j] = (shifted >> (j * 7)) & 0x7F;
}
pos += bpw;
}
return dataBytes;
}
printProperties() {
const rate = this.samplePeriod > 0 ? Math.round(1000000000 / this.samplePeriod) : 0;
console.log(`Device ID: ${this.deviceId}`);
console.log(`Sample Number: ${this.sampleNumber}`);
console.log(`Significant Bits per Sample: ${this.significantBits}`);
console.log(`Sample Period (ns): ${this.samplePeriod}`);
console.log(`Sample Rate (Hz): ${rate}`);
console.log(`Sample Length (words): ${this.sampleLength}`);
console.log(`Loop Start (words): ${this.loopStart}`);
console.log(`Loop End (words): ${this.loopEnd}`);
console.log(`Loop Type: ${this.loopType}`);
console.log(`Sample Data Length: ${this.sampleData.length}`);
}
}
// Usage:
// const sds = new SDSFile();
// await sds.load('example.sds');
// sds.printProperties();
// sds.sampleNumber = 1;
// await sds.save('modified.sds');
- C++ Class
Uses<fstream>andstd::vector. Similar logic.
#include <fstream>
#include <vector>
#include <iostream>
#include <stdexcept>
#include <cstring>
class SDSFile {
public:
int device_id = 0;
int sample_number = 0;
int significant_bits = 0;
int sample_period = 0;
int sample_length = 0;
int loop_start = 0;
int loop_end = 0;
int loop_type = 0;
std::vector<int> sample_data;
void load(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) throw std::runtime_error("File not found");
std::vector<unsigned char> data((std::istreambuf_iterator<char>(file)), {});
size_t pos = 0;
bool header_found = false;
int packet_num = 0;
sample_data.clear();
while (pos < data.size()) {
if (data[pos] == 0xF0) {
pos++;
if (pos + 3 >= data.size()) break;
if (data[pos++] != 0x7E) continue;
int dev = data[pos++];
int sub = data[pos++];
std::vector<unsigned char> message;
while (pos < data.size() && data[pos] != 0xF7) message.push_back(data[pos++]);
if (pos < data.size() && data[pos++] == 0xF7) {
if (sub == 1 && message.size() == 16 && !header_found) {
header_found = true;
device_id = dev;
sample_number = message[0] + (message[1] << 7);
significant_bits = message[2];
sample_period = message[3] + (message[4] << 7) + (message[5] << 14);
sample_length = message[6] + (message[7] << 7) + (message[8] << 14);
loop_start = message[9] + (message[10] << 7) + (message[11] << 14);
loop_end = message[12] + (message[13] << 7) + (message[14] << 14);
loop_type = message[15];
} else if (sub == 2 && message.size() == 122 && header_found) {
int kk = message[0];
if (kk != packet_num % 128) throw std::runtime_error("Packet mismatch");
unsigned char data_bytes[120];
std::memcpy(data_bytes, &message[1], 120);
int checksum = message[121];
int calc = 0x7E ^ dev ^ 0x02 ^ kk;
for (int i = 0; i < 120; i++) calc ^= data_bytes[i];
calc &= 0x7F;
if (calc != checksum) throw std::runtime_error("Checksum error");
auto unpacked = unpack_packet(data_bytes, 120);
sample_data.insert(sample_data.end(), unpacked.begin(), unpacked.end());
packet_num++;
}
}
} else pos++;
}
if (sample_data.size() != static_cast<size_t>(sample_length)) throw std::runtime_error("Length mismatch");
}
private:
int get_bytes_per_word() const {
int ee = significant_bits;
if (ee == 0) return 0;
return ((ee - 1) / 7) + 1;
}
std::vector<int> unpack_packet(const unsigned char* data_bytes, int len) const {
int bpw = get_bytes_per_word();
int points = len / bpw;
std::vector<int> samples;
int pos = 0;
for (int p = 0; p < points; p++) {
int value = 0;
for (int j = 0; j < bpw; j++) {
value = (value << 7) | data_bytes[pos++];
}
int shift = bpw * 7 - significant_bits;
value >>= shift;
samples.push_back(value);
}
return samples;
}
public:
void save(const std::string& path) const {
std::ofstream file(path, std::ios::binary);
int bpw = get_bytes_per_word();
int points_per_packet = 120 / bpw;
// Header
unsigned char header[21] = {0xF0, 0x7E, static_cast<unsigned char>(device_id), 0x01};
int idx = 4;
header[idx++] = sample_number & 0x7F;
header[idx++] = (sample_number >> 7) & 0x7F;
header[idx++] = significant_bits & 0x7F;
header[idx++] = sample_period & 0x7F;
header[idx++] = (sample_period >> 7) & 0x7F;
header[idx++] = (sample_period >> 14) & 0x7F;
header[idx++] = sample_length & 0x7F;
header[idx++] = (sample_length >> 7) & 0x7F;
header[idx++] = (sample_length >> 14) & 0x7F;
header[idx++] = loop_start & 0x7F;
header[idx++] = (loop_start >> 7) & 0x7F;
header[idx++] = (loop_start >> 14) & 0x7F;
header[idx++] = loop_end & 0x7F;
header[idx++] = (loop_end >> 7) & 0x7F;
header[idx++] = (loop_end >> 14) & 0x7F;
header[idx++] = loop_type & 0x7F;
header[idx++] = 0xF7;
file.write(reinterpret_cast<char*>(header), 21);
// Packets
for (size_t i = 0; i < sample_data.size(); i += points_per_packet) {
size_t chunk_size = std::min(points_per_packet, static_cast<int>(sample_data.size() - i));
unsigned char packed[120] = {0};
pack_packet(&sample_data[i], chunk_size, packed);
int kk = static_cast<int>(i / points_per_packet) % 128;
int calc = 0x7E ^ device_id ^ 0x02 ^ kk;
for (int j = 0; j < 120; j++) calc ^= packed[j];
unsigned char packet[125] = {0xF0, 0x7E, static_cast<unsigned char>(device_id), 0x02, static_cast<unsigned char>(kk)};
std::memcpy(&packet[5], packed, 120);
packet[125] = calc & 0x7F;
packet[5 + 120] = calc & 0x7F;
packet[5 + 120 + 1] = 0xF7;
file.write(reinterpret_cast<char*>(packet), 127);
}
}
void pack_packet(const int* samples, size_t num, unsigned char* data_bytes) const {
int bpw = get_bytes_per_word();
int pos = 0;
for (size_t s = 0; s < num; s++) {
int value = samples[s];
int shift = bpw * 7 - significant_bits;
int shifted = value << shift;
for (int j = bpw - 1; j >= 0; j--) {
data_bytes[pos + j] = (shifted >> (j * 7)) & 0x7F;
}
pos += bpw;
}
}
void print_properties() const {
int rate = sample_period > 0 ? 1000000000 / sample_period : 0;
std::cout << "Device ID: " << device_id << std::endl;
std::cout << "Sample Number: " << sample_number << std::endl;
std::cout << "Significant Bits per Sample: " << significant_bits << std::endl;
std::cout << "Sample Period (ns): " << sample_period << std::endl;
std::cout << "Sample Rate (Hz): " << rate << std::endl;
std::cout << "Sample Length (words): " << sample_length << std::endl;
std::cout << "Loop Start (words): " << loop_start << std::endl;
std::cout << "Loop End (words): " << loop_end << std::endl;
std::cout << "Loop Type: " << loop_type << std::endl;
std::cout << "Sample Data Length: " << sample_data.size() << std::endl;
}
};
// Usage:
// SDSFile sds;
// sds.load("example.sds");
// sds.print_properties();
// sds.sample_number = 1;
// sds.save("modified.sds");