Is there a faster way to read and process an aiff files sound data

I have a program that reads an aiff format music file. It works but I wonder is there a faster way to read and process the sound data. An aiff file is broken up into chunks and the SSND chunk holds the music data. I have a loop that steps through the file and uses a match statement to process each chunk. The code associated with the match arm for SSND is below. The code reads the lower half of the data and the upper half of the data into separate buffers then passes each one into a separate thread. Each thread processes the data then returns two vectors, one for the left channel and one for the right channel (the music is 2 channel stereo). The associated vectors are appended and I end up with 2 vectors, one for each channel. Any thoughts on a better/faster approach will be appreciated.

println!("----- SSND Chunk -----");
println!("ckSize (chunk size) = {:?}", ck_size);
// ckSize (chunk size) =  64,585,928 bytes

let mut ck_data = [0u8; 8];
file.read_exact(&mut ck_data)?;
let offset = u32::from_be_bytes(ck_data[0..4].try_into().unwrap_or_default());
let _block_size = u32::from_be_bytes(ck_data[4..8].try_into().unwrap_or_default());

if offset > 0 {
    file.seek(SeekFrom::Current(offset as i64))?;
}

let half_sound_bytes = (ck_size - 8 - offset) / 2;

println!("half sound bytes = {:?}", half_sound_bytes);
// half sound bytes =     32,292,960 bytes = ckSize - 8

let mut raw_bytes1 = vec![0u8; half_sound_bytes as usize];
file.read_exact(&mut raw_bytes1)?;

let mut raw_bytes2 = vec![0u8; half_sound_bytes as usize];
file.read_exact(&mut raw_bytes2)?;

let channel_data1 = Arc::new(raw_bytes1);
let cloned_channel_data1 = Arc::clone(&channel_data1);

let channel_data2 = Arc::new(raw_bytes2);
let cloned_channel_data2 = Arc::clone(&channel_data2);

let channel_handle1 = thread::spawn( move || {

    let mut left_chnl = Vec::new();
    let mut right_chnl = Vec::new();

    for chunk in cloned_channel_data1.chunks_exact(4) {

    let left_sample = i16::from_be_bytes(chunk[0..2].try_into().unwrap_or_default());
    let right_sample = i16::from_be_bytes(chunk[2..4].try_into().unwrap_or_default());

    left_chnl.push(left_sample);
    right_chnl.push(right_sample);
    
    }
    (left_chnl, right_chnl)
});

let channel_handle2 = thread::spawn( move || {

    let mut left_chnl = Vec::new();
    let mut right_chnl = Vec::new();

    for chunk in cloned_channel_data2.chunks_exact(4) {

    let left_sample = i16::from_be_bytes(chunk[0..2].try_into().unwrap_or_default());
    let right_sample = i16::from_be_bytes(chunk[2..4].try_into().unwrap_or_default());

    left_chnl.push(left_sample);
    right_chnl.push(right_sample);
    
    }
    (left_chnl, right_chnl)
});

let channels_lower = channel_handle1.join().unwrap();
let channels_upper = channel_handle2.join().unwrap();

let mut left1 = channels_lower.0;
let mut right1 = channels_lower.1;
let mut left2 = channels_upper.0;
let mut right2 = channels_upper.1;

left1.append(&mut left2);
right1.append(&mut right2);

println!("left channel length = {:?}", left1.len());
println!("right channel length = {:?}", right1.len());
// left channel length =  16,146,480 bytes
// right channel length = 16,146,480 bytes

Use memmap with chunk if it is in Linux. It is zero copy and still able to handle file bigger than RAM size because the data is not loaded to RAM all at once

Here is the gap between memmap and std::fs

File size 100 MB

Iteration 10.000

Total time std::fs : 2.834935769s
Average per iteration : 283.493µs

Total time memmap2 : 87.616µs
Average per iteration : 8ns

use memmap2::Mmap;
use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom};
use std::time::Instant;

const FILE_PATH: &str = "sample_bench_data.bin";
const FILE_SIZE: u64 = 100 * 1024 * 1024;
const CHUNK_SIZE: usize = 4096;
const OFFSET: u64 = 50 * 1024 * 1024;
const ITERATIONS: u32 = 10_000;

fn main() -> std::io::Result<()> {
    println!("Creating sample file ({} MB)...", FILE_SIZE / (1024 * 1024));
    let file_setup = File::create(FILE_PATH)?;
    file_setup.set_len(FILE_SIZE)?;
    drop(file_setup);

    println!("\nRunning std::fs benchmark ({} iterations)...", ITERATIONS);
    let start_fs = Instant::now();
    for _ in 0..ITERATIONS {
        let mut file = File::open(FILE_PATH)?;
        let mut buffer = vec![0u8; CHUNK_SIZE];
        file.seek(SeekFrom::Start(OFFSET))?;
        file.read_exact(&mut buffer)?;
        std::hint::black_box(buffer);
    }
    let duration_fs = start_fs.elapsed();

    println!("Running memmap2 benchmark ({} iterations)...", ITERATIONS);
    let file_for_mmap = File::open(FILE_PATH)?;
    let mmap = unsafe { Mmap::map(&file_for_mmap)? };
    let start_mmap = Instant::now();
    for _ in 0..ITERATIONS {
        let start_pos = OFFSET as usize;
        let end_pos = start_pos + CHUNK_SIZE;
        let chunk = &mmap[start_pos..end_pos];
        std::hint::black_box(chunk);
    }
    let duration_mmap = start_mmap.elapsed();

    println!("\nTotal time std::fs : {:?}", duration_fs);
    println!("Average per iteration std::fs : {:?}", duration_fs / ITERATIONS);
    println!("\nTotal time memmap2 : {:?}", duration_mmap);
    println!("Average per iteration memmap2 : {:?}", duration_mmap / ITERATIONS);
    
    if duration_mmap < duration_fs {
        let speedup = duration_fs.as_nanos() as f64 / duration_mmap.as_nanos() as f64;
        println!("\n-> memmap2 is approx {:.2}x faster than std::fs", speedup);
    }

    fs::remove_file(FILE_PATH)?;
    Ok(())
}

Also you do not need Arc there

Use std::thread::scope, It is just normal reference so it is faster than Arc. I feel like std::thread::scope is unpopular in Rust user

Also in your case, there is a way to know the allocation size needed. So we can use ::with_capacity to prealloc heap. This will avoid Vec's reallocation continously in the loop

println!("----- SSND Chunk -----");
println!("ckSize (chunk size) = {:?}", ck_size);

let mut ck_data = [0u8; 8];
file.read_exact(&mut ck_data)?;
let offset = u32::from_be_bytes(ck_data[0..4].try_into().unwrap_or_default());
let _block_size = u32::from_be_bytes(ck_data[4..8].try_into().unwrap_or_default());

if offset > 0 {
    file.seek(SeekFrom::Current(offset as i64))?;
}

let half_sound_bytes = (ck_size - 8 - offset) / 2;

println!("half sound bytes = {:?}", half_sound_bytes);

// calculate capacity for prealloc heap
let sample_capacity = (half_sound_bytes as usize) / 4;

let mut raw_bytes1 = vec![0u8; half_sound_bytes as usize];
file.read_exact(&mut raw_bytes1)?;

let mut raw_bytes2 = vec![0u8; half_sound_bytes as usize];
file.read_exact(&mut raw_bytes2)?;

let (mut left1, mut right1, mut left2, mut right2) = thread::scope(|s| {
    let handle1 = s.spawn(|| {
        let mut left_chnl = Vec::with_capacity(sample_capacity * 2);
        let mut right_chnl = Vec::with_capacity(sample_capacity * 2);

        for chunk in raw_bytes1.chunks_exact(4) {
            let left_sample = i16::from_be_bytes(chunk[0..2].try_into().unwrap_or_default());
            let right_sample = i16::from_be_bytes(chunk[2..4].try_into().unwrap_or_default());

            left_chnl.push(left_sample);
            right_chnl.push(right_sample);
        }
        (left_chnl, right_chnl)
    });

    let handle2 = s.spawn(|| {
        let mut left_chnl = Vec::with_capacity(sample_capacity);
        let mut right_chnl = Vec::with_capacity(sample_capacity);

        for chunk in raw_bytes2.chunks_exact(4) {
            let left_sample = i16::from_be_bytes(chunk[0..2].try_into().unwrap_or_default());
            let right_sample = i16::from_be_bytes(chunk[2..4].try_into().unwrap_or_default());

            left_chnl.push(left_sample);
            right_chnl.push(right_sample);
        }
        (left_chnl, right_chnl)
    });

    let (l1, r1) = handle1.join().unwrap();
    let (l2, r2) = handle2.join().unwrap();

    (l1, r1, l2, r2)
});

left1.append(&mut left2);
right1.append(&mut right2);

println!("left channel length = {:?}", left1.len());
println!("right channel length = {:?}", right1.len());

I will look at MMAP. Based on your example it appears to be incredibly fast. In the example code directly above, when you allocate the vectors they are different in the 2 threads. The total number of bytes of sound data is equal to (ck_size - 8 - offset). It would seem to me that I should allocate 1/4 of that for each of the 4 vectors? I could achieve this by using (half_sound_bites as usize / 2). Regarding not using Arc, I thought I had to create a reference to the data and move that into the thread. I am clearly misunderstanding a concept.

I think it's mostly as simple as being much newer to std, and thus less well-known than thread::spawn (with a dose of "definitely do not want to wait for threads to finish").

You can send references that borrow locals (&_, &mut _) with thread::scope instead of the non-borrowing, owning references (Arc<_>) that thread::spawn requires.[1] This is possible because thread::scope doesn't return until all of the new threads have completed (so any locals which have been borrowed cannot have been destructed, etc). In your OP you were waiting for the other threads to finish before doing anything else anyway, so there's no downside.


  1. by having a Closure: 'static bound ↩︎

Yeah, because memmap is zero copy and doesn't do continous syscall. In memmap, the syscall is only 1 time at the setup, then afterward it is zero syscall. While std::fs does continous syscall

In your case, the total size is

size = size - 8 - offset

Then you read (size / 2) bytes from the file 2 times, vec A and vec B

Then you spawn 2 thread, each will return 2 vec. So all threads returns 4 vec

Then you append 2 vec from thread B ro 2 vec from thread A

Because in appending vec to vec, if the destination vec does not has enough capacity, it will realloc

So the vec for thread A must have double size of the vec for thread B

size_for_thread_A = size / 2

size_for_thread_B = size / 4

So when you append vec from thread B to vec from thread A, there will no realloc because vec from thread A already has capacity needed to save the result from vec thread B

Your solution was spot on except there is one thing I do not understand. The total bytes of sound data are 64,585,920 and I may not have made this very clear in my original post. This is made up of 4 byte frames, 2 bytes for the left channel and 2 bytes for the right channel. So the total number of elements in a Vec is half of the total bytes. If you break out the left and right channels to separate vectors, each will contain 1/4 of the total bytes. So in handle 1 thread wouldn't you set vector capacity to 1/4 of total bytes and in handle 2 wouldn't you set vector capacity to 1/8 of total bytes and still only allocate once? I am basing this on my understanding that Vec::with_capacity(xyz) sets vector size in terms of elements not bytes but my understanding could be wrong.
BTW, next I plan to try your recommendation of memmap. It looks like once setup you can treat the file like an array and access an element or ranges of elements. I assume this will work on a Mac.

64,585,920	Total bytes of sound data ie chunk size
32,292,960	Total elements of type i16 (1/2 of total)
16,146,480	Total left channel elements (1/4 of total, handle 1 thread)
16,146,480	Total right channel elements (1/4 of total, handle 1 thread)
8,073,240	Left channel elements (1/8 of total, handle 2 thread)
8,073,240	Right channel elements (1/8 of total, handle 2 thread)