Contents



Introduction

Most Rust tutorials covering writing to a file convert writing text to a file using the write or writeln macro to write strings. A typical example may look like this:

use std::fs::File;
use std::io::prelude::*;

fn main() -> std::io::Result<()> {
	let mut file = File::create("out.txt")?;
	write!(&mut file, "Hello, ")?;
	writeln!(&mut file, " World!")?;
    Ok(())
}

Another example would be using write_all:

use std::fs::File;
use std::io::prelude::*;

fn main() -> std::io::Result<()> {
	let mut file = File::create("out.txt")?;
	file.write_all("Goodbye".as_bytes())?;
    Ok(())
}

However, creating a WAV (Waveform Audio File Format) is a much more suitable example to explore a function to write to files. We have to face endianness (WAV files are of type little endian) and locate certain positions in a file that need to be changed from placeholders to important data after the sound data was added. NB!: This is not an introduction to the WAV audio format.

Step by Step Example

Unlike other programming languages, Rust does not differentiate between opening a file as “text” or “binary” data stream. Every file opened is considered binary (bytes). The source code below is certainly not optimized and looks rather long but it demonstrates a few very useful function calls and considerations when writing (binary) files with rust.

Let’s start with the header. Parts of the header contain data which is basically ASCII characters. We can write to file using either the write! macro, or output_file.write_all("TEXT".as_bytes()) or passing the byte array directly (output_file.write_all(&[0x52,0x49,0x46,0x46])). Sometimes it is important to set a placeholder, in this case done using output_file.write_all("----".as_bytes()) to change it later. In order to so, we need to record the position. This can be done using output_file.stream_position().

As long as we are writing vector views of bytes (&[u8]) to a file, everything is good. However, we may need to write chunks of data that are not of type u8 but of type u16 or u32 which consist of multiple bytes. Luckily, these data types provide a function call to convert it into byte vectors .to_be_bytes and .to_le_bytes. be stands for big-endian and le stands for little-endian which are simply two different ways of ordering bytes. The WAV file format assumes that everything is stored as little endian. Therefore, we are going to use output_file.write_all(&CHUNKSIZE.to_le_bytes()).

The WAV file example contains two placeholder we need to access after all data is written to the output file. The positions are recorded as type u64 and refer to the start of the file. Therefore, we change the position using output_file.seek(SeekFrom::Start(pos_data_placeholder)) to change to the position we want to write the next bytes to.

If we would use a programming language such as C++, then we would call file.close before terminating the program to ensure all bytes are written properly. In this example we simple sync it (output_file.sync_all()). A crate called close-file seems to provide some support for closing files properly.

The result sounds like this:

Source Code

use std::fs::File;
use std::io::prelude::*;
use std::io::SeekFrom;


const PI:f64 = 3.1415926535;
const BITDEPTH:u16 = 16;
const SAMPLERATE:u32 = 44100;
const CHANNELS:u16 = 1;
const BLOCKALIGN:u16 = BITDEPTH/2;
const BYTERATE:u32 = SAMPLERATE*BITDEPTH as u32/8;
const FORMAT:u16 = 1; // WAVE_FORMAT_PCM
const CHUNKSIZE:u32 = 16;
const DURATION:u8 = 2;
const FREQUENCY:f64 = 150.0;

fn main() -> std::io::Result<()> {
    // open file
    let mut output_file = File::create("wav_file_with_rust_sample.wav")?;
    
    // Header
    // - RIFF
    output_file.write_all(&[0x52,0x49,0x46,0x46])?;
    // - ---- place holder
    let pos_cksize = output_file.stream_position()?;
    output_file.write_all("----".as_bytes())?;
    output_file.write_all("WAVE".as_bytes())?;
    
    //  Format
    output_file.write_all("fmt ".as_bytes())?;
    output_file.write_all(&CHUNKSIZE.to_le_bytes())?;
    output_file.write_all(&FORMAT.to_le_bytes())?;
    output_file.write_all(&CHANNELS.to_le_bytes())?;
    output_file.write_all(&SAMPLERATE.to_le_bytes())?;
    output_file.write_all(&BYTERATE.to_le_bytes())?;
    output_file.write_all(&BLOCKALIGN.to_le_bytes())?;
    output_file.write_all(&BITDEPTH.to_le_bytes())?;
    
    // Data
    output_file.write_all("data".as_bytes())?;
    let pos_data_placeholder = output_file.stream_position()?;
    output_file.write_all("----".as_bytes())?;
    let pos_data_start = output_file.stream_position()?;
    // generate some sine wave
    let amplitude:f64 = 0.5;
    let offset:f64 = 2.0*PI*FREQUENCY/(SAMPLERATE as f64);
    let mut angle:f64 = 0.0;
    let samples_required:u64 = SAMPLERATE as u64 * DURATION as u64;
    
    let mut sample:f64;
    let mut sample_to_write:i16;
    let max_amplitude:f64 = 2.0f64.powi((BITDEPTH-1).into()) - 1.0;
    
    for _ in 1..samples_required
    {
        sample = amplitude * angle.sin();
        angle += offset;
        sample_to_write = (sample * max_amplitude) as i16;
        output_file.write_all(&sample_to_write.to_le_bytes())?;
    }
    let mut pos_end = output_file.stream_position()?;
    
    let chunk_size_data:u32 = (pos_end - pos_data_start) as u32;
    if chunk_size_data % 2 != 0 {
        output_file.write_all(&[0x00])?;
        pos_end = output_file.stream_position()?;
    }
    output_file.seek(SeekFrom::Start(pos_data_placeholder))?;
	
	output_file.write_all(&chunk_size_data.to_le_bytes())?;
	
    output_file.seek(SeekFrom::Start(pos_cksize))?;
    let chunk_size_header:u32 = (pos_end - 8) as u32;
    output_file.write_all(&chunk_size_header.to_le_bytes())?;
    
    output_file.sync_all()?;
    Ok(())
}