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(())
}