🙆

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