noise の秋ですね Rust で実装しませんか?
はじめに
まったく涼しくありませんがもう秋ですね。
日が短くなり、どこか物悲しい季節には noise がぴったりです。
以前 Node.js で noise を鑑賞するコマンドを作って noise を楽しんでいました。
しかし intel mac から Apple Silicon の mac に買い替えたところ動かなくなり、しょうがなく YouTube で雨の音や宇宙の音を聞いていました。
う〜ん、コマンドラインで noise が聞きたくなりますね。秋ですし。
za-
今回は Rust で noise コマンドを作ってみました。
3つの種類の noise を再生することが出来ます。
- white: 荒々しい noise
- pink: やや控えめだがキレのある noise
- brown: まろやかでコクのある noise
結構かんたんにできるので、よかったら一緒に作ってみませんか?
準備
最終的なファイル配置は次のようになります。
.
├── Cargo.lock
├── Cargo.toml
└── src
├── main.rs
├── noise
│ ├── brown.rs
│ ├── mod.rs
│ ├── pink.rs
│ └── white.rs
└── types.rs
Rust を install しましょう。
その後プロジェクト設定を行います。
# 初期化
cargo init za-
cd za-
# ランダムのライブラリをインストール
cargo add rand
# オーディオのライブラリをインストール
cargo add rodio
ここまでできたら Cargo.toml が次のようになっています。
[package]
name = "za-"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.9.2"
rodio = "0.21.1"
src/types.rs
先に型が決まるとスムーズに進むので types.rs を作りましょう。
use std::fmt;
/// noise の state
#[derive(Debug)]
pub enum State {
/// white は state 保持不要
White,
/// Pink は7つの独立したランダム値を異なる頻度で更新して1/f特性を作る
Pink { values: [f32; 7], counter: usize },
/// Brown は前回の値に微小な変化を加えるランダムウォーク
Brown { value: f32 },
}
pub struct Noise {
pub sample_rate: u32,
pub state: State,
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
State::White => write!(f, "White"),
State::Pink { .. } => write!(f, "Pink"),
State::Brown { .. } => write!(f, "Brown"),
}
}
}
src/noise/
続いて noise の本体を作りましょう。
mkdir src/noise
cd noise
touch white.rs pink.rs brown.rs
src/noise 下に3つのファイルができました。
それぞれ実装していきましょう。
まずは簡単な white.rs から
use rand::prelude::*;
pub fn generate<R: Rng>(rng: &mut R) -> Option<f32> {
Some(rng.random_range(-0.5..0.5))
}
-0.5 から 0.5 までのランダムを返すだけの簡単な関数ですね。
次に難しい pink.rs
use rand::prelude::*;
pub fn generate<R: Rng>(rng: &mut R, values: &mut [f32; 7], counter: &mut usize) -> Option<f32> {
for i in 0..7 {
if *counter & (1 << i) == 0 {
values[i] = rng.random_range(-1.0..1.0);
}
}
*counter += 1;
let sum: f32 = values.iter().sum();
Some(sum * 0.1)
}
ちょっと難しそうに見えますが、仕組みはシンプルです。
7つの独立したランダム値 (values) を保持し counter のビットパターンを見て、各値を異なる頻度で更新します。
- values[0] は毎フレーム更新(counter の 0 ビット目は常に交互に 0/1)
- values[1] は 2 フレームごと
- values[2] は 4 フレームごと
という具合に倍々で更新頻度が下がり、全ての値を合計して出力します。
この「異なる時間スケールで変化する値の合成」により、自然界でよく見られる 1/f 特性が生まれます。
雨音や風の音に近い、聴き疲れしにくい noise になります。
最後に brown.rs
use rand::prelude::*;
pub fn generate<R: Rng>(rng: &mut R, value: &mut f32) -> Option<f32> {
*value += rng.random_range(-0.1..0.1);
*value = value.clamp(-1.0, 1.0);
Some(*value * 0.3)
}
ブラウンノイズは「ランダムウォーク」で生成します。
前回の値に小さな変化を加えていくだけなので、ゆったりとした変化になります。
海の波のような、低周波成分が多い落ち着いた noise です。
src/noise/mod.rs
3つの noise を統合する mod.rs を作りましょう。
pub mod brown;
pub mod pink;
pub mod white;
use crate::types::{Noise, State};
use rodio::Source;
use std::time::Duration;
impl Noise {
pub fn new(state: State) -> Self {
Self {
sample_rate: 44100,
state,
}
}
pub fn white() -> Self {
Self::new(State::White)
}
pub fn pink() -> Self {
Self::new(State::Pink {
values: [0.0; 7],
counter: 0,
})
}
pub fn brown() -> Self {
Self::new(State::Brown { value: 0.0 })
}
}
impl Iterator for Noise {
type Item = f32;
fn next(&mut self) -> Option<Self::Item> {
let mut rng = rand::rng();
match &mut self.state {
State::White => white::generate(&mut rng),
State::Pink { values, counter } => pink::generate(&mut rng, values, counter),
State::Brown { value } => brown::generate(&mut rng, value),
}
}
}
impl Source for Noise {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> u16 {
1
}
fn sample_rate(&self) -> u32 {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
rodio の Source トレイトを実装することで、音声として再生できるようになります。
Iterator として無限にサンプルを生成し続けるのがポイントです。
src/main.rs
エントリーポイントを作りましょう。
use rodio::{OutputStreamBuilder, Sink};
mod noise;
mod types;
use types::Noise;
fn main() {
let stream = OutputStreamBuilder::open_default_stream().unwrap_or_else(|e| {
println!("Could not open audio stream: {}", e);
std::process::exit(1);
});
let sink = Sink::connect_new(stream.mixer());
println!("1 - White");
println!("2 - Pink");
println!("3 - Brown");
println!("q - Quit");
let play = |noise: Noise| {
println!("{}", noise.state);
sink.stop();
sink.append(noise);
};
loop {
let mut input = String::new();
match std::io::stdin().read_line(&mut input) {
Ok(0) => break,
Ok(_) => {}
Err(e) => {
println!("ERROR: {}", e);
break;
}
}
let command = input.trim();
match command {
"1" => play(Noise::white()),
"2" => play(Noise::pink()),
"3" => play(Noise::brown()),
"q" => break,
_ => println!("???"),
}
}
drop(stream);
}
実行してみよう
cargo r
で実行します。
すると
1 - White
2 - Pink
3 - Brown
q - Quit
と表示されます。
1, 2, 3, を切り替えてみましょう。
飽きたら q で終了です。
コマンドラインにしてみよう
やっぱりターミナルで za-
と入れると noise が再生されるようにしたいですね。秋ですし。
Cargo.toml に以下を追加しましょう。
[[bin]]
name = "za-"
path = "src/main.rs"
その後 cargo install --path .
を実行することで za-
が登録されます。
さいごに
ゆっくり noise を楽しみましょう。
僕は飽きたので YouTube で違う音楽を聴いてきます。
Discussion