Skip to content

Commit 85b8635

Browse files
committed
Merge branch 'feature/wav-output'
2 parents 3b8a06e + e0be2d9 commit 85b8635

3 files changed

Lines changed: 141 additions & 49 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
/target
2+
3+
.vscode/

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A command-line tool to generate sine wave audio buffers.
88
- **Bit Depths**: 16-bit, 24-bit, 32-bit audio
99
- **Channel Configurations**: Mono (1 channel) or Stereo (2 channels)
1010
- **Custom Duration**: Generate any length of audio in milliseconds
11-
- **Multiple Output Formats**: Hex, C arrays, Rust arrays, raw binary
11+
- **Multiple Output Formats**: Hex, C arrays, Rust arrays, raw binary, Waveform Audio File Format (PCM)
1212
- **Analysis Mode**: Calculate buffer requirements and efficiency
1313

1414
## Use Cases
@@ -54,6 +54,11 @@ cargo build --release
5454

5555
# Raw binary output (pipe to file)
5656
./sine_generator -r 16000 -d 10 -o raw > sinewave.bin
57+
58+
# Wav output (pipe to file)
59+
./sine_generator -d 1000 -f 1000 -o wav > sinewave.wav
60+
61+
5762
```
5863

5964
### Command Line Options
@@ -144,8 +149,6 @@ Duration: 1.0 ms
144149
Buffer Analysis:
145150
Samples: 16
146151
Total bytes: 64
147-
USB packets: 1 (64 bytes each)
148-
Efficiency: 100.0%
149152
150153
Frequency Analysis:
151154
Period: 36.36 samples

main.rs

Lines changed: 133 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::env;
2-
use std::f32::consts::PI;
2+
use std::f32::consts::TAU;
3+
use std::io::Write;
34
use std::process;
5+
use std::vec::Vec;
46

57
static SUPPORTED_SAMPLE_RATES: [u32; 3] = [
68
16_000, // 16 kHz is commonly used for speech and telephony applications
@@ -43,15 +45,54 @@ impl SampleWidth {
4345
}
4446
}
4547

48+
// https://ccrma.stanford.edu/courses/422-winter-2014/projects/WaveFormat/
49+
#[repr(C, packed)]
50+
#[allow(dead_code)]
51+
struct WavHeader {
52+
chunk_id: [u8; 4], // 0
53+
chunk_size: u32, //4
54+
format: [u8; 4], //8
55+
subchunk_1_id: [u8; 4], //12
56+
subchunk_1_size: u32, // 16
57+
audio_format: u16, // 20
58+
num_channels: u16, // 22
59+
sample_rate: u32, // 24
60+
byte_rate: u32, // 28
61+
block_align: u16, // 32
62+
bits_per_sample: u16, // 34
63+
subchunk_2_id: [u8; 4], //36
64+
subchunk_2_size: u32, //40
65+
}
66+
67+
impl WavHeader {
68+
pub fn new() -> Self {
69+
Self {
70+
chunk_id: *b"RIFF",
71+
chunk_size: 0,
72+
format: *b"WAVE",
73+
subchunk_1_id: *b"fmt ",
74+
subchunk_1_size: 16,
75+
audio_format: 0x0001, //WINDOWS PCM
76+
num_channels: 1,
77+
sample_rate: 44_100,
78+
byte_rate: 176_400,
79+
block_align: 2,
80+
bits_per_sample: 16,
81+
subchunk_2_id: *b"data",
82+
subchunk_2_size: 0,
83+
}
84+
}
85+
}
86+
4687
// Get the maximum absolute value for a given sample width.
4788
// Digital Audio Representation:
4889
/*
4990
|----------|-----------------------------|------------------|
50-
| Format | Integer Type | Max Positive |
91+
| Format | Integer Type | Max Positive |
5192
|----------|-----------------------------|------------------|
52-
| 16-bit | int16_t | 32767 |
53-
| 24-bit | int32_t (in 24 bits) | 8,388,607 |
54-
| 32-bit | int32_t | 2,147,483,647 |
93+
| 16-bit | int16_t | 32767 |
94+
| 24-bit | int32_t (in 24 bits) | 8,388,607 |
95+
| 32-bit | int32_t | 2,147,483,647 |
5596
|----------|-----------------------------|------------------|
5697
*/
5798
fn get_range(sample_width: SampleWidth) -> f32 {
@@ -79,6 +120,7 @@ enum OutputFormat {
79120
RustArray,
80121
RawBytes,
81122
Info,
123+
WavFile,
82124
}
83125

84126
impl OutputFormat {
@@ -89,12 +131,13 @@ impl OutputFormat {
89131
"rustarray" | "rust" => Some(OutputFormat::RustArray),
90132
"raw" | "bytes" => Some(OutputFormat::RawBytes),
91133
"info" => Some(OutputFormat::Info),
134+
"wav" => Some(OutputFormat::WavFile),
92135
_ => None,
93136
}
94137
}
95138
}
96139

97-
fn print_usage() {
140+
fn print_usage() {
98141
println!("Usage: singen [OPTIONS]");
99142
println!();
100143
println!("Options:");
@@ -109,6 +152,7 @@ fn print_usage() {
109152
println!(" carray - C-style array declaration");
110153
println!(" rustarray - Rust array declaration");
111154
println!(" raw - Raw binary bytes (stdout)");
155+
println!(" wav - Windows audio file format (stdout)");
112156
println!(" info - Only show buffer info, no data");
113157
println!(" -a, --analyze Analyze only (don't generate data)");
114158
println!(" -h, --help Show this help message");
@@ -219,44 +263,47 @@ fn parse_args() -> Config {
219263
config
220264
}
221265

222-
// Create a sine wave audio buffer for a given frequency, sample rate, channel count, and sample width.
223-
fn create_sine_array(
224-
freq: f32,
225-
sample_rate: f32,
226-
channel_count: u8,
227-
sample_width: SampleWidth,
228-
duration_ms: f32,
229-
) -> (Vec<u8>, usize, usize) {
230-
// Calculate the phase increment for each sample and the total number of samples needed for the specified duration
231-
let mut phase: f32 = 0.0;
232-
let phase_inc: f32 = freq / sample_rate * 2.0 * PI; // Radians per sample
233-
let total_samples: usize = ((duration_ms * sample_rate) / 1000.0).round() as usize; // Number of samples in the specified duration
234-
235-
// If USB packet mode, adjust to fit 64-byte packets
236-
let bytes_per_sample = sample_width as usize;
237-
let bytes_per_frame = bytes_per_sample * channel_count as usize;
238-
let total_bytes = total_samples * bytes_per_frame;
239-
240-
// Pre-allocate buffer with the total number of bytes needed for the sine wave
241-
let mut buffer = Vec::with_capacity(total_bytes);
242-
let max_value = get_range(sample_width);
243-
244-
// Fill buffer with sine wave
245-
for _ in 0..total_samples {
246-
let sample = (phase.sin() * max_value) as i32;
247-
let bytes = sample.to_le_bytes();
248-
249-
for _ in 0..channel_count {
250-
for n in 0..sample_width as usize {
251-
buffer.push(bytes[n]);
266+
/// Generate a linear chirp from `f0` Hz to `f1` Hz over `duration_secs`.
267+
/// Returns a vector of floating‑point samples in the range [-1.0, 1.0].
268+
fn generate_linear_chirp(
269+
f0: f32, // start frequency (Hz)
270+
f1: f32, // end frequency (Hz)
271+
sample_rate: f32, // samples per second
272+
duration_secs: f32, // total duration in seconds
273+
) -> Vec<f32> {
274+
let dt = 1.0 / sample_rate;
275+
let num_samples = (duration_secs * sample_rate).round() as usize;
276+
let mut samples = Vec::with_capacity(num_samples);
277+
let mut phase = 0.0;
278+
279+
for i in 0..num_samples {
280+
let t = i as f32 * dt;
281+
// Instantaneous frequency at time t (linear interpolation)
282+
let freq = f0 + (f1 - f0) * (t / duration_secs);
283+
// Phase increment for this sample
284+
phase += TAU * freq * dt;
285+
// Keep phase in [-π, π] range to avoid floating-point drift (optional)
286+
phase = phase.rem_euclid(TAU);
287+
samples.push(phase.sin());
288+
}
289+
290+
samples
291+
}
292+
293+
fn float_samples_to_bytes(samples: &[f32], channels: u8, sample_width: SampleWidth) -> Vec<u8> {
294+
let max_val = get_range(sample_width);
295+
let mut buffer = Vec::with_capacity(samples.len() * channels as usize * sample_width as usize);
296+
297+
for &sample in samples {
298+
let scaled = (sample * max_val).round() as i32;
299+
let bytes = scaled.to_le_bytes();
300+
for _ in 0..channels {
301+
for b in &bytes[0..sample_width as usize] {
302+
buffer.push(*b);
252303
}
253304
}
254-
255-
// Keep phase reset when it exceeds 2PI to prevent discontinuities at the reset point
256-
phase = (phase + phase_inc) % (2.0 * PI);
257305
}
258-
259-
(buffer, total_samples, total_bytes)
306+
buffer
260307
}
261308

262309
fn print_buffer_info(config: &Config, total_samples: usize, total_bytes: usize) {
@@ -287,7 +334,7 @@ fn print_buffer_info(config: &Config, total_samples: usize, total_bytes: usize)
287334
println!(
288335
" Full cycles: {:.2}",
289336
total_samples as f32 / period_samples
290-
);
337+
);
291338
}
292339

293340
fn print_buffer_hex(buffer: &[u8], bytes_per_line: usize) {
@@ -393,16 +440,47 @@ fn print_raw_bytes(buffer: &[u8]) {
393440
handle.write_all(buffer).unwrap();
394441
}
395442

443+
fn create_wav_file_array(
444+
buffer: &[u8],
445+
sample_rate: u32,
446+
channels: u16,
447+
sample_width: SampleWidth,
448+
) -> Vec<u8> {
449+
let wav_header_len = std::mem::size_of::<WavHeader>();
450+
let buffer_len = buffer.len();
451+
452+
let mut wav_hdr = WavHeader::new();
453+
wav_hdr.chunk_size = (36 + buffer_len) as u32; // 4 + (24) + 8 + buffer_len
454+
wav_hdr.num_channels = channels;
455+
wav_hdr.sample_rate = sample_rate;
456+
wav_hdr.byte_rate = sample_rate as u32 * channels as u32 * sample_width as u32;
457+
wav_hdr.block_align = channels * sample_width as u16; // fixed formula
458+
wav_hdr.bits_per_sample = sample_width as u16 * 8;
459+
wav_hdr.subchunk_2_size = buffer_len as u32;
460+
461+
let mut file = Vec::with_capacity(wav_header_len + buffer_len);
462+
let ptr = &wav_hdr as *const WavHeader as *const u8;
463+
// SAFETY: WavHeader is repr(C, packed) so it has no padding.
464+
file.write_all(unsafe { std::slice::from_raw_parts(ptr, wav_header_len) })
465+
.unwrap();
466+
file.write_all(buffer).unwrap();
467+
file
468+
}
469+
396470
fn main() {
397471
let config = parse_args();
398472

399-
let (buffer, total_samples, total_bytes) = create_sine_array(
473+
let total_samples =
474+
((config.duration_ms * config.sample_rate as f32) / 1000.0).round() as usize;
475+
let total_bytes = total_samples * (config.sample_width as u8 * config.channels) as usize;
476+
477+
let float_samples = generate_linear_chirp(
478+
config.frequency,
400479
config.frequency,
401480
config.sample_rate as f32,
402-
config.channels,
403-
config.sample_width,
404-
config.duration_ms,
481+
config.duration_ms / 1000.0,
405482
);
483+
let buffer = float_samples_to_bytes(&float_samples, config.channels, config.sample_width);
406484

407485
match config.output_format {
408486
OutputFormat::Info => {
@@ -426,5 +504,14 @@ fn main() {
426504
OutputFormat::RawBytes => {
427505
print_raw_bytes(&buffer);
428506
}
507+
OutputFormat::WavFile => {
508+
let file = create_wav_file_array(
509+
&buffer,
510+
config.sample_rate,
511+
config.channels as u16,
512+
config.sample_width,
513+
);
514+
print_raw_bytes(file.as_ref());
515+
}
429516
}
430517
}

0 commit comments

Comments
 (0)