Some questions on idioms in a small program


#1

Hey y’all! I’m new to rust but so far so good!

I am working on a small program for learning on, and I have some questions about idioms.

it lives here.

Currently this is a program that produces a wav file of white noise that lasts for one second.

Some important notes:

  1. #[allow(unused_must_use)] is intentional, I plan to learn about result types
    and error handling before removing it. This is also why none of the stdout
    writes are wrapped in try!

  2. This will write garbage to your terminal and corrupt your session if you
    cargo run it. This is also intentional although not the eventually desired
    behavior. You can cargo build it and then redirect the binary’s output to a
    wav file ./target/debug/rav > thingy.wav which is then playable in a
    player if you’re into white noise. You can also change the duration in the
    main function.

So! some questions I have about idiomatic rust! How would I, instead of passing a handle to a StdoutLock into write_header, pass what I would think of in C as a file pointer? Can the write header function accept either one? do they share a common trait they can be typed against? How can I idiomatically parse 'argv' to make the duration variable based on the first argument? It comes in as a string, right? how can I default it to 1 if there is no input? are the type casts as u16 really necessary? that feels icky to me but I don’t see another way to do it in the header writer.

I have found “solutions” of sorts to all these problems, but I’m too new to rust to really have a good compass on what is well structured rust code. So any wise eyes are appreciated!

Thank you for any input!


#2

For the first question, I would pass a &mut Write. That is the trait that StdoutLock and File implements, so you could call the function for writing either on stdout or on a specific file (Edit: or lots of other “Writable” stuff, including Vec<u8>, which is great for testing).

For the second question, it is possible to do something like argv.nth(1).unwrap_or("1".into()).parse::<usize>(), but I would recommend using a good command line parser like clap instead.


#3

Thanks @kaj that’s exactly what I was looking for on both counts.

I still have to get used to the use of packages!


#4

Your code looks good, but it feels a bit cramped:

extern crate rand;
extern crate byteorder;

use std::io::{ Write };
use byteorder::{LittleEndian, WriteBytesExt};

const SAMPLE_RATE: u32 = 44100;
const CHANNELS: u32 = 1;
const HEADER_SIZE: u32 = 36;
const SUBCHUNK1_SIZE: u32 = 16;
const AUDIO_FORMAT: u32 = 1;
const BIT_DEPTH: u32 = 8;
const BYTE_SIZE: u32 = 8;

#[allow(unused_must_use)]
fn write_header<T: Write>(seconds: u32, handle: &mut T) {
    let numsamples = SAMPLE_RATE * seconds;

    handle.write(b"RIFF");
  
    handle.write_u32::<LittleEndian>(HEADER_SIZE + numsamples);
  
    handle.write(b"WAVEfmt ");
  
    handle.write_u32::<LittleEndian>(SUBCHUNK1_SIZE);
    handle.write_u16::<LittleEndian>(AUDIO_FORMAT as u16);
    handle.write_u16::<LittleEndian>(CHANNELS as u16);
    handle.write_u32::<LittleEndian>(SAMPLE_RATE);
    handle.write_u32::<LittleEndian>(SAMPLE_RATE * CHANNELS * (BIT_DEPTH / BYTE_SIZE));
    handle.write_u16::<LittleEndian>((CHANNELS * (BIT_DEPTH / BYTE_SIZE)) as u16);
    handle.write_u16::<LittleEndian>(BIT_DEPTH as u16);
  
    handle.write(b"data");
  
    handle.write_u32::<LittleEndian>(numsamples * CHANNELS * (BIT_DEPTH / BYTE_SIZE));
}

#[allow(unused_must_use)]
fn make_some_noise<T: Write>(seconds: u32, handle: &mut T) {
    for _ in 0..seconds * SAMPLE_RATE {
        handle.write(&[ rand::random::<u8>() ]);
    }
}

#[allow(unused_must_use)]
fn main() {
    let duration = 1;
    let mut vec: Vec<u8> = Vec::new();
  
    write_header(duration, &mut vec);
    make_some_noise(duration, &mut vec);
  
    println!("{:?}", &vec);
}

Added some more newlines (“coding in paragraphs”) and T:Foo -> T: Foo.

I also agree with what @kaj said.


#5

The remaining issue is the as u16 etc in the code. I would probably have defined the constants in their “natural” type, but then I would have to convert other usage instead, like so:

const SAMPLE_RATE: u32 = 44100;
const CHANNELS: u16 = 1;
...
handle.write_u16::<LittleEndian>(CHANNELS);
handle.write_u32::<LittleEndian>(SAMPLE_RATE);
handle.write_u32::<LittleEndian>(SAMPLE_RATE * (CHANNELS as u32) * (...));

I really think u32 * u16 and u32 * u8 should be allowed, but I don’t know a way to allow them, and I assume there is some good reason for not allowing them even though I havn’t found it yet … :slight_smile:


#6

I… think I agree with you re: casting similarly signed numerical types? But I would be totally interested in learning why that particular explicitness is necessary! It’s certainly in line with the rest of the syntax, though the as construct feels a little strange in the middle of an equation. I’m sure a lot of that could be smoothed out by casting into a local binding before plugging it into the expression.

Also your suggestion to write the constants in their native types is :thumbsup: