///|
/// Writes a 16-bit unsigned integer into FixedArray[Byte] in Little-Endian order.
pub fn write_u16_le(b : FixedArray[Byte], offset : Int32, val : Int32) -> Unit {
b[offset] = (val & 0xff).to_byte()
b[offset + 1] = ((val >> 8) & 0xff).to_byte()
}
///|
/// Writes a 32-bit integer into FixedArray[Byte] in Little-Endian order.
pub fn write_u32_le(b : FixedArray[Byte], offset : Int32, val : Int32) -> Unit {
b[offset] = (val & 0xff).to_byte()
b[offset + 1] = ((val >> 8) & 0xff).to_byte()
b[offset + 2] = ((val >> 16) & 0xff).to_byte()
b[offset + 3] = ((val >> 24) & 0xff).to_byte()
}
///|
/// Encodes an AudioBuffer into standard 16-bit signed PCM Microsoft RIFF WAVE binary bytes.
pub fn encode_wav_16bit(buffer : AudioBuffer) -> Bytes {
let num_samples = buffer.length()
let sample_rate = buffer.sample_rate
let num_channels = 1
let bits_per_sample = 16
let byte_rate = sample_rate * num_channels * bits_per_sample / 8
let block_align = num_channels * bits_per_sample / 8
let data_size = num_samples * block_align
let chunk_size = 36 + data_size
let total_size = 44 + data_size
let b = FixedArray::make(total_size, b'\x00')
// 00..03: "RIFF"
b[0] = b'R'
b[1] = b'I'
b[2] = b'F'
b[3] = b'F'
// 04..07: ChunkSize
write_u32_le(b, 4, chunk_size)
// 08..11: "WAVE"
b[8] = b'W'
b[9] = b'A'
b[10] = b'V'
b[11] = b'E'
// 12..15: "fmt "
b[12] = b'f'
b[13] = b'm'
b[14] = b't'
b[15] = b' '
// 16..19: Subchunk1Size (16 for PCM)
write_u32_le(b, 16, 16)
// 20..21: AudioFormat (1 = PCM linear)
write_u16_le(b, 20, 1)
// 22..23: NumChannels
write_u16_le(b, 22, num_channels)
// 24..27: SampleRate
write_u32_le(b, 24, sample_rate)
// 28..31: ByteRate
write_u32_le(b, 28, byte_rate)
// 32..33: BlockAlign
write_u16_le(b, 32, block_align)
// 34..35: BitsPerSample
write_u16_le(b, 34, bits_per_sample)
// 36..39: "data"
b[36] = b'd'
b[37] = b'a'
b[38] = b't'
b[39] = b'a'
// 40..43: Subchunk2Size
write_u32_le(b, 40, data_size)
// 44..end: PCM samples
let samples = buffer.samples
for i = 0; i < num_samples; i = i + 1 {
let mut s = samples[i]
if s > 1.0 {
s = 1.0
} else if s < -1.0 {
s = -1.0
}
let pcm = (s * 32767.0).to_int()
write_u16_le(b, 44 + i * 2, pcm)
}
let list = []
for byte in b {
list.push(byte)
}
Bytes::from_array(list)
}
///|
/// Encodes an AudioBuffer into standard 8-bit unsigned PCM Microsoft RIFF WAVE binary bytes.
pub fn encode_wav_8bit(buffer : AudioBuffer) -> Bytes {
let num_samples = buffer.length()
let sample_rate = buffer.sample_rate
let num_channels = 1
let bits_per_sample = 8
let byte_rate = sample_rate * num_channels * bits_per_sample / 8
let block_align = num_channels * bits_per_sample / 8
let data_size = num_samples * block_align
let chunk_size = 36 + data_size
let total_size = 44 + data_size
let b = FixedArray::make(total_size, b'\x00')
// RIFF header
b[0] = b'R'
b[1] = b'I'
b[2] = b'F'
b[3] = b'F'
write_u32_le(b, 4, chunk_size)
b[8] = b'W'
b[9] = b'A'
b[10] = b'V'
b[11] = b'E'
// fmt chunk
b[12] = b'f'
b[13] = b'm'
b[14] = b't'
b[15] = b' '
write_u32_le(b, 16, 16)
write_u16_le(b, 20, 1)
write_u16_le(b, 22, num_channels)
write_u32_le(b, 24, sample_rate)
write_u32_le(b, 28, byte_rate)
write_u16_le(b, 32, block_align)
write_u16_le(b, 34, bits_per_sample)
// data chunk
b[36] = b'd'
b[37] = b'a'
b[38] = b't'
b[39] = b'a'
write_u32_le(b, 40, data_size)
// 8-bit unsigned PCM samples [0, 255]
let samples = buffer.samples
for i = 0; i < num_samples; i = i + 1 {
let mut s = samples[i]
if s > 1.0 {
s = 1.0
} else if s < -1.0 {
s = -1.0
}
let pcm = ((s + 1.0) * 127.5).to_int()
let clamped = if pcm < 0 { 0 } else if pcm > 255 { 255 } else { pcm }
b[44 + i] = clamped.to_byte()
}
let list = []
for byte in b {
list.push(byte)
}
Bytes::from_array(list)
}
///|
let base64_table : FixedArray[Char] = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
]
///|
/// Encodes raw binary bytes into a standard RFC 4648 Base64 string.
pub fn to_base64(bytes : Bytes) -> String {
let len = bytes.length()
if len == 0 {
return ""
}
let sb = StringBuilder()
let mut i = 0
while i < len {
let b0 = bytes[i].to_int() & 0xff
let b1 = if i + 1 < len { bytes[i + 1].to_int() & 0xff } else { 0 }
let b2 = if i + 2 < len { bytes[i + 2].to_int() & 0xff } else { 0 }
let triplet = (b0 << 16) | (b1 << 8) | b2
let c0 = (triplet >> 18) & 0x3f
let c1 = (triplet >> 12) & 0x3f
let c2 = (triplet >> 6) & 0x3f
let c3 = triplet & 0x3f
sb.write_char(base64_table[c0])
sb.write_char(base64_table[c1])
if i + 1 < len {
sb.write_char(base64_table[c2])
} else {
sb.write_char('=')
}
if i + 2 < len {
sb.write_char(base64_table[c3])
} else {
sb.write_char('=')
}
i = i + 3
}
sb.to_string()
}
///|
/// Encodes an AudioBuffer as a self-contained Base64 Data URI string ("data:audio/wav;base64,...").
pub fn AudioBuffer::to_data_uri(self : AudioBuffer) -> String {
let wav_bytes = encode_wav_16bit(self)
"data:audio/wav;base64," + to_base64(wav_bytes)
}
///|
/// Encodes a StereoBuffer into standard 2-channel 16-bit signed PCM Microsoft RIFF WAVE binary bytes.
pub fn encode_wav_stereo_16bit(stereo : StereoBuffer) -> Bytes {
let num_samples = stereo.length()
let sample_rate = stereo.sample_rate
let num_channels = 2
let bits_per_sample = 16
let block_align = num_channels * bits_per_sample / 8 // 4 bytes per frame
let byte_rate = sample_rate * block_align
let data_size = num_samples * block_align
let chunk_size = 36 + data_size
let total_size = 44 + data_size
let b = FixedArray::make(total_size, b'\x00')
// 00..03: "RIFF"
b[0] = b'R'
b[1] = b'I'
b[2] = b'F'
b[3] = b'F'
// 04..07: ChunkSize
write_u32_le(b, 4, chunk_size)
// 08..11: "WAVE"
b[8] = b'W'
b[9] = b'A'
b[10] = b'V'
b[11] = b'E'
// 12..15: "fmt "
b[12] = b'f'
b[13] = b'm'
b[14] = b't'
b[15] = b' '
// 16..19: Subchunk1Size (16 for PCM)
write_u32_le(b, 16, 16)
// 20..21: AudioFormat (1 = PCM linear)
write_u16_le(b, 20, 1)
// 22..23: NumChannels (2)
write_u16_le(b, 22, num_channels)
// 24..27: SampleRate
write_u32_le(b, 24, sample_rate)
// 28..31: ByteRate
write_u32_le(b, 28, byte_rate)
// 32..33: BlockAlign (4)
write_u16_le(b, 32, block_align)
// 34..35: BitsPerSample (16)
write_u16_le(b, 34, bits_per_sample)
// 36..39: "data"
b[36] = b'd'
b[37] = b'a'
b[38] = b't'
b[39] = b'a'
// 40..43: Subchunk2Size
write_u32_le(b, 40, data_size)
// 44..end: Interleaved PCM samples (L0, R0, L1, R1, ...)
let left_samples = stereo.left.samples
let right_samples = stereo.right.samples
for i = 0; i < num_samples; i = i + 1 {
let mut sl = if i < left_samples.length() { left_samples[i] } else { 0.0 }
if sl > 1.0 {
sl = 1.0
} else if sl < -1.0 {
sl = -1.0
}
let pcml = (sl * 32767.0).to_int()
write_u16_le(b, 44 + i * 4, pcml)
let mut sr = if i < right_samples.length() { right_samples[i] } else { 0.0 }
if sr > 1.0 {
sr = 1.0
} else if sr < -1.0 {
sr = -1.0
}
let pcmr = (sr * 32767.0).to_int()
write_u16_le(b, 44 + i * 4 + 2, pcmr)
}
let list = []
for byte in b {
list.push(byte)
}
Bytes::from_array(list)
}
///|
/// Encodes a StereoBuffer as a self-contained Base64 Data URI string.
pub fn StereoBuffer::to_data_uri(self : StereoBuffer) -> String {
let wav_bytes = encode_wav_stereo_16bit(self)
"data:audio/wav;base64," + to_base64(wav_bytes)
}
///|
/// Metadata information parsed from a standard RIFF WAVE file header.
#derive(Eq, Debug)
pub struct WavInfo {
sample_rate : Int32
num_channels : Int32
bits_per_sample : Int32
num_samples : Int32
duration : Double
}
///|
/// Reads a 16-bit unsigned integer from Bytes in Little-Endian order.
fn read_u16_le(bytes : Bytes, offset : Int32) -> Int32 {
let b0 = bytes[offset].to_int() & 0xff
let b1 = bytes[offset + 1].to_int() & 0xff
b0 | (b1 << 8)
}
///|
/// Reads a 32-bit unsigned integer from Bytes in Little-Endian order.
fn read_u32_le(bytes : Bytes, offset : Int32) -> Int32 {
let b0 = bytes[offset].to_int() & 0xff
let b1 = bytes[offset + 1].to_int() & 0xff
let b2 = bytes[offset + 2].to_int() & 0xff
let b3 = bytes[offset + 3].to_int() & 0xff
b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
}
///|
/// Inspects metadata from a RIFF WAVE byte stream without fully decoding all audio samples.
pub fn inspect_wav(bytes : Bytes) -> Result[WavInfo, String] {
if bytes.length() < 44 {
return Err("Invalid WAV file: size is less than 44-byte standard header")
}
// Check "RIFF"
if bytes[0] != b'R' ||
bytes[1] != b'I' ||
bytes[2] != b'F' ||
bytes[3] != b'F' {
return Err("Invalid WAV file: missing 'RIFF' magic signature")
}
// Check "WAVE"
if bytes[8] != b'W' ||
bytes[9] != b'A' ||
bytes[10] != b'V' ||
bytes[11] != b'E' {
return Err("Invalid WAV file: missing 'WAVE' format signature")
}
// Search for fmt and data chunks
let mut offset = 12
let mut fmt_found = false
let mut data_found = false
let mut num_channels = 1
let mut sample_rate = 44100
let mut bits_per_sample = 16
let mut data_size = 0
while offset + 8 <= bytes.length() {
let c0 = bytes[offset]
let c1 = bytes[offset + 1]
let c2 = bytes[offset + 2]
let c3 = bytes[offset + 3]
let chunk_len = read_u32_le(bytes, offset + 4)
if chunk_len < 0 {
return Err("Corrupted WAV file: invalid negative chunk length")
}
if c0 == b'f' && c1 == b'm' && c2 == b't' && c3 == b' ' {
fmt_found = true
let audio_format = read_u16_le(bytes, offset + 8)
if audio_format != 1 {
return Err(
"Unsupported WAV compression format: only uncompressed PCM (format=1) is supported",
)
}
num_channels = read_u16_le(bytes, offset + 10)
sample_rate = read_u32_le(bytes, offset + 12)
bits_per_sample = read_u16_le(bytes, offset + 22)
} else if c0 == b'd' && c1 == b'a' && c2 == b't' && c3 == b'a' {
data_found = true
data_size = chunk_len
break
}
let padded_len = if chunk_len % 2 == 1 { chunk_len + 1 } else { chunk_len }
offset = offset + 8 + padded_len
}
if !fmt_found {
return Err("Corrupted WAV file: 'fmt ' subchunk not found")
}
if !data_found {
return Err("Corrupted WAV file: 'data' subchunk not found")
}
let bytes_per_sample = bits_per_sample / 8
let block_align = num_channels * bytes_per_sample
let num_samples = if block_align > 0 { data_size / block_align } else { 0 }
let duration = if sample_rate > 0 {
num_samples.to_double() / sample_rate.to_double()
} else {
0.0
}
Ok({ sample_rate, num_channels, bits_per_sample, num_samples, duration, })
}
///|
/// Decodes a RIFF WAVE byte stream into a mono AudioBuffer.
/// If the file is stereo, both channels are averaged into mono.
pub fn decode_wav(bytes : Bytes) -> Result[AudioBuffer, String] {
let info = match inspect_wav(bytes) {
Ok(inf) => inf
Err(e) => return Err(e)
}
if info.bits_per_sample != 16 && info.bits_per_sample != 8 {
return Err(
"Unsupported bit depth: only 16-bit signed and 8-bit unsigned PCM are supported",
)
}
// Find data offset
let mut offset = 12
let mut data_offset = 44
while offset + 8 <= bytes.length() {
if bytes[offset] == b'd' &&
bytes[offset + 1] == b'a' &&
bytes[offset + 2] == b't' &&
bytes[offset + 3] == b'a' {
data_offset = offset + 8
break
}
let chunk_len = read_u32_le(bytes, offset + 4)
if chunk_len < 0 {
break
}
let padded_len = if chunk_len % 2 == 1 { chunk_len + 1 } else { chunk_len }
offset = offset + 8 + padded_len
}
let samples = Array::make(info.num_samples, 0.0)
let bytes_per_sample = info.bits_per_sample / 8
let block_align = info.num_channels * bytes_per_sample
if info.bits_per_sample == 16 {
for i = 0; i < info.num_samples; i = i + 1 {
let frame_offset = data_offset + i * block_align
if frame_offset + block_align > bytes.length() {
break
}
if info.num_channels == 1 {
let raw = read_u16_le(bytes, frame_offset)
let signed_val = if raw >= 32768 { raw - 65536 } else { raw }
samples[i] = signed_val.to_double() / 32768.0
} else {
// Average left and right
let raw_l = read_u16_le(bytes, frame_offset)
let s_l = if raw_l >= 32768 { raw_l - 65536 } else { raw_l }
let raw_r = read_u16_le(bytes, frame_offset + 2)
let s_r = if raw_r >= 32768 { raw_r - 65536 } else { raw_r }
samples[i] = (s_l.to_double() + s_r.to_double()) / (2.0 * 32768.0)
}
}
} else {
// 8-bit unsigned PCM
for i = 0; i < info.num_samples; i = i + 1 {
let frame_offset = data_offset + i * block_align
if frame_offset + block_align > bytes.length() {
break
}
if info.num_channels == 1 {
let b = bytes[frame_offset].to_int() & 0xff
samples[i] = (b.to_double() - 128.0) / 128.0
} else {
let bl = bytes[frame_offset].to_int() & 0xff
let br = bytes[frame_offset + 1].to_int() & 0xff
let sl = (bl.to_double() - 128.0) / 128.0
let sr = (br.to_double() - 128.0) / 128.0
samples[i] = (sl + sr) * 0.5
}
}
}
Ok({ sample_rate: info.sample_rate, samples, })
}
///|
/// Decodes a 2-channel RIFF WAVE byte stream into a StereoBuffer.
pub fn decode_wav_stereo(bytes : Bytes) -> Result[StereoBuffer, String] {
let info = match inspect_wav(bytes) {
Ok(inf) => inf
Err(e) => return Err(e)
}
if info.num_channels != 2 {
return Err(
"Expected 2-channel stereo WAV file, but found channel count: " +
info.num_channels.to_string(),
)
}
if info.bits_per_sample != 16 && info.bits_per_sample != 8 {
return Err("Unsupported bit depth: only 16-bit and 8-bit are supported")
}
let mut offset = 12
let mut data_offset = 44
while offset + 8 <= bytes.length() {
if bytes[offset] == b'd' &&
bytes[offset + 1] == b'a' &&
bytes[offset + 2] == b't' &&
bytes[offset + 3] == b'a' {
data_offset = offset + 8
break
}
let chunk_len = read_u32_le(bytes, offset + 4)
if chunk_len < 0 {
break
}
let padded_len = if chunk_len % 2 == 1 { chunk_len + 1 } else { chunk_len }
offset = offset + 8 + padded_len
}
let left_samples = Array::make(info.num_samples, 0.0)
let right_samples = Array::make(info.num_samples, 0.0)
let bytes_per_sample = info.bits_per_sample / 8
let block_align = info.num_channels * bytes_per_sample
if info.bits_per_sample == 16 {
for i = 0; i < info.num_samples; i = i + 1 {
let frame_offset = data_offset + i * block_align
if frame_offset + block_align > bytes.length() {
break
}
let raw_l = read_u16_le(bytes, frame_offset)
let sl = if raw_l >= 32768 { raw_l - 65536 } else { raw_l }
let raw_r = read_u16_le(bytes, frame_offset + 2)
let sr = if raw_r >= 32768 { raw_r - 65536 } else { raw_r }
left_samples[i] = sl.to_double() / 32768.0
right_samples[i] = sr.to_double() / 32768.0
}
} else {
for i = 0; i < info.num_samples; i = i + 1 {
let frame_offset = data_offset + i * block_align
if frame_offset + block_align > bytes.length() {
break
}
let bl = bytes[frame_offset].to_int() & 0xff
let br = bytes[frame_offset + 1].to_int() & 0xff
left_samples[i] = (bl.to_double() - 128.0) / 128.0
right_samples[i] = (br.to_double() - 128.0) / 128.0
}
}
Ok({
sample_rate: info.sample_rate,
left: { sample_rate: info.sample_rate, samples: left_samples, },
right: { sample_rate: info.sample_rate, samples: right_samples, },
})
}