~~
「~~(なみなみ)」は眺めているだけで心癒される。そして少し哲学的、科学的に思いを馳せることもできるかもしれない波を楽しむための装置です。
皆さんが波という言葉を聞いて連想するとき、そのイメージはポジティブな印象、例えば雄大な海の波を想起される方が多いのでh内科と思います。。
ゆったりと大きく力強い、時には繊細な波が繰り返し打ち寄せる。でもよくよく見ると二度と同じ波は来ない。
そんなイメージの波を卓上でお手軽に楽しんでみたいな。という思いから作った装置です。
ユニット交換式の卓上装置
「~~」は電子制御を行うコントローラ部、モーターによる駆動部を隠したドライバ、透明のケースの中に造波装置を埋め込んだなみなみユニットの三つの部位に分かれています。
なみなみユニットは現在4台が稼働しています。
ポンと乗せ換えるだけで交換できて、動力は磁力を介して伝達しています。
ハードウェア構成図
SPRESENSEでステッピングモーターを制御し、ロータリーエンコーダーでモーターの回転数を変更できます。
SPRESENSEと拡張基板、DC-DC降圧ボードは3Dプリンタ製のベースボードの上に固定していますが、筐体のカバーは設置せず基板はむき出し。ディスプレイとロータリーエンコーダは固定せずケーブル接続しただけです。
ステッピングモーターは駆動部の筐体の中に隠し、外部からは見えないものの。なみなみユニットに動力を伝えていることは自明かと思います。
なみなみユニットは現在4種類をラインナップしています。駆動ユニットとは磁石で結合し、伝達された回転運動を種々の運動に変換して波を起こしています。
なみなみユニット
なみなみユニットの動きは上の動画の通りです。
物体Aの運動と波及
一つ目の装置は、一目みただけで、波を作る機構部が波を起こす様子を理解できます。
ダイナミックかつシンプルな波です。
しかしながら細部を見ると同じ波は一つとして存在していないことにも気づきます。
シンプルなオブジェクトの動きが、シンプルさと複雑さの両方を引き起こす波の魅力の根幹を表しています
1/323,323
二つ目の装置は、4枚のフィンの組み合わせがランダムな波を起こすことを狙っています。
フィンを駆動する歯車はそれぞれ異なる素数枚の歯をもっており、323,323回転に1回だけフィンが同期します。
シンプルな回転運動が静かではあるけれど複雑な状態を引き起こすという構造により、複雑な現象とメカ的な機構の関係にフォーカスします。
運動の推定
三つ目の装置では、再びダイナミックな波を起こしていますが、波を起こす機構部は隠されています。
波の形をみただけで、波の原因となる機構が想像しやすいシンプルな形の波だけが見えることで、波という現象の裏には波を作る機構が必ず存在しているという因果関係を想像させています。
回転運動の影響
四つ目の装置には一見造波機構は見当たりません。装置を動かすとユニットそのものが回転し始め、波を生み出します。
なみなみユニットの中だけで完結していた事象を、ユニットという枠組みそのものを波を作る機構に取り込むことで作品のリフレーミングを促します。
メディアアートとしての「~~」
「~~」は、波を眺めて楽しむための卓上装置として制作しました。しかし、4つのなみなみユニットを連作として鑑賞すると、この装置は単なる“癒やしのオブジェ”ではなく、波という現象そのものの成り立ちを考えさせるメディアとして立ち上がってきます。
私たちは普段、波を「結果」として見ています。
海の波、風に揺れる水面、音の波、光の波。
そこに必ず原因があることは知っていても、その仕組みを意識することはほとんどありません。
「~~」では、あえて波だけが目に入る構成をとっています。
なみなみユニットに近づくほど、内部の造波機構は隠され、観察者が直接見られるのは波の形とその変化だけです。一方で、電子制御部や配線、基板といった「本来は隠されがちな部分」はあえて露出させています。
この配置は偶然ではありません。
見えないものが、現象を生む
なみなみユニットの中で起きていることは、ほとんどの場合、外からは分かりません。
それでも確かに波は生まれ、揺れ、変化し続けます。
これは、海の波ととてもよく似ています。
海の波も、風や潮汐、地形といった複雑な要因によって生まれますが、私たちが目にするのはあくまで「波」という現象だけです。
「~~」は、この見えない因果関係と、見える現象の関係を、卓上サイズの装置で二重に表現しています。
なみなみユニットの内部に隠された物理的な造波機構
電子制御によってモーターを駆動し、その運動を生み出している制御系
どちらも直接は見えない、しかし確実に現象を支えている存在です。
ユニット交換という行為の意味
なみなみユニットは、ポンと乗せ換えるだけで交換できます。
操作としてはとても単純ですが、行っていることは少し変わっています。
ユニットを交換するという行為は、
波の見た目を変えているのではなく、波を生み出す因果関係そのものを差し替えているのです。
プログラムを書き換えているわけでも、数値パラメータを調整しているわけでもありません。
歯車の歯数、回転の位相、運動の変換方法といった、極めて物理的な構造そのものがユニットの中に封じ込められています。
その結果として現れる波は、同じ装置、同じ液体でありながら、まったく異なる性格を持ちます。
波を「記録する」メディアとして
この構造を考えているうちに、私はこの装置を波の記録メディアとして捉えるようになりました。
音楽を例にすると分かりやすいかもしれません。
カセットテープやレコードは、音そのものを直接扱うのではなく、音を生み出す情報や構造を物理的に記録しています。再生装置にかけることで、初めて音として立ち上がります。
なみなみユニットも、それとよく似ています。
ユニットの中に記録されているのは「波」ではない
記録されているのは「波を生み出す構造」
ユニットを交換すると、波が変わる。
それは、別の波を“再生”しているというより、別の因果関係を再生していると言った方が近いかもしれません。
デジタルでもなく、数値化もしていない。
非常に原始的で、完全にアナログな、波の記録装置です。
卓上サイズの自然現象
「~~」は、海の波のスケールをそのまま再現しようとした装置ではありません。
むしろ、自然現象が持つ構造そのものを縮尺して持ち込むことを目指しています。
同じ条件でも二度と同じ波は現れないこと
単純な運動が、複雑な結果を生むこと
見えない構造が、確かに現象を支えていること
それらを、静かに、しかし確実に感じ取れる卓上装置としてまとめました。
もし「~~」を前にして、
「なぜこの波が生まれているのだろう」
「中では何が起きているのだろう」
と少しでも想像してもらえたなら、この装置は役割を果たしていると思います。
波を眺めながら、原因に思いを巡らせる。
そんな時間を楽しんでもらえたら嬉しいです。
コード
/*
Copyright (c) 2019, miya
Modified by airpocket, 2025
All rights reserved.
(BSD 2-Clause License)
*/
#include <MediaPlayer.h>
#include <OutputMixer.h>
#include <MemoryUtil.h>
#include <math.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// ======= ILI9341 SPI接続ピン定義 =======
#define TFT_CS 10 // Chip Select
#define TFT_DC 9 // Data/Command
#define TFT_RST 8 // Reset(必要なければ省略可)
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
const int MIXER_VOLUME = -160;
const int32_t S_BUFFER_SIZE = 8192;
uint8_t s_buffer[S_BUFFER_SIZE];
bool err_flag = false;
// ======= 波形パラメータ =======
const float SAMPLE_RATE = 48000.0f;
float freq = 440.0f; // 初期周波数(Hz)
int volume = 50; // 0〜100%
int amplitude = 0; // 0〜32767
float phase = 0.0f;
const float PI2 = 6.2831853f;
// ======= Audio関連 =======
MediaPlayer *player;
OutputMixer *mixer;
// ======= 入力ピン設定 =======
const int POT_FREQ = A0;
const int POT_VOL = A1;
const float FREQ_MIN = 1.0f;
const float FREQ_MAX = 1000.0f;
const int ADC_FREQ_MIN = 30;
const int ADC_FREQ_MAX = 610;
const int ADC_VOL_MIN = 30;
const int ADC_VOL_MAX = 610;
// ======= コールバック =======
static void error_callback(const ErrorAttentionParam *errparam)
{
if (errparam->error_code > AS_ATTENTION_CODE_WARNING) {
err_flag = true;
}
}
static void mixer_done_callback(MsgQueId id, MsgType type, AsOutputMixDoneParam *param)
{
return;
}
static void mixer_send_callback(int32_t id, bool is_end)
{
AsRequestNextParam next;
next.type = (!is_end) ? AsNextNormalRequest : AsNextStopResRequest;
AS_RequestNextPlayerProcess(AS_PLAYER_ID_0, &next);
return;
}
static bool player_done_callback(AsPlayerEvent event, uint32_t result, uint32_t sub_result)
{
return true;
}
// ======= 波形生成 =======
void generate_sine(int16_t *buf, uint32_t frames)
{
amplitude = (int)(32767.0f * (volume / 100.0f));
float phase_inc = PI2 * freq / SAMPLE_RATE;
for (uint32_t i = 0; i < frames; i++) {
int16_t v = (int16_t)(sin(phase) * amplitude);
buf[i * 2 + 0] = v;
buf[i * 2 + 1] = v;
phase += phase_inc;
if (phase >= PI2) phase -= PI2;
}
}
// ======= PCMコールバック =======
void player_decode_callback(AsPcmDataParam pcm_param)
{
int16_t *buf = (int16_t*)pcm_param.mh.getPa();
uint32_t frames = pcm_param.size / 4;
generate_sine(buf, frames);
mixer->sendData(OutputMixer0, mixer_send_callback, pcm_param);
}
// ======= 初期化 =======
void setup()
{
Serial.begin(115200);
// --- TFT 初期化 ---
tft.begin();
tft.setRotation(1); // 横向き
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(20, 20);
tft.println("Spresense Sine Generator");
tft.setTextSize(2);
tft.setCursor(20, 50);
tft.println("Initializing audio...");
// --- Audio初期化 ---
initMemoryPools();
createStaticPools(MEM_LAYOUT_PLAYER);
player = MediaPlayer::getInstance();
mixer = OutputMixer::getInstance();
player->begin();
mixer->activateBaseband();
player->create(MediaPlayer::Player0, error_callback);
mixer->create(error_callback);
player->activate(MediaPlayer::Player0, player_done_callback);
mixer->activate(OutputMixer0, mixer_done_callback);
usleep(100 * 1000);
player->init(MediaPlayer::Player0, AS_CODECTYPE_WAV, "/mnt/sd0/BIN",
AS_SAMPLINGRATE_48000, AS_BITLENGTH_16, AS_CHANNEL_STEREO);
mixer->setVolume(MIXER_VOLUME, 0, 0);
memset(s_buffer, 0, sizeof(s_buffer));
player->writeFrames(MediaPlayer::Player0, s_buffer, S_BUFFER_SIZE);
player->start(MediaPlayer::Player0, player_decode_callback);
tft.fillScreen(ILI9341_BLACK);
Serial.println("[OK] Audio initialized.");
}
// ======= メインループ =======
void loop()
{
player->writeFrames(MediaPlayer::Player0, s_buffer, S_BUFFER_SIZE);
// --- 周波数制御 ---
int rawFreq = analogRead(POT_FREQ);
if (rawFreq <= ADC_FREQ_MIN) freq = FREQ_MIN;
else if (rawFreq >= ADC_FREQ_MAX) freq = FREQ_MAX;
else freq = FREQ_MIN + (FREQ_MAX - FREQ_MIN) *
((float)(rawFreq - ADC_FREQ_MIN) / (ADC_FREQ_MAX - ADC_FREQ_MIN));
// --- ボリューム制御 ---
int rawVol = analogRead(POT_VOL);
if (rawVol <= ADC_VOL_MIN) volume = 0;
else if (rawVol >= ADC_VOL_MAX) volume = 100;
else volume = (int)(100.0f * (float)(rawVol - ADC_VOL_MIN) / (ADC_VOL_MAX - ADC_VOL_MIN));
// --- 表示更新 ---
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 200) {
lastUpdate = millis();
// シリアル出力
Serial.print("[A0]="); Serial.print(rawFreq);
Serial.print(" [A1]="); Serial.print(rawVol);
Serial.print(" -> [freq]="); Serial.print(freq, 1);
Serial.print(" Hz [vol]="); Serial.print(volume);
Serial.print("% [amp]="); Serial.println(amplitude);
// TFT出力
tft.fillRect(0, 0, 320, 240, ILI9341_BLACK);
tft.setCursor(20, 30); tft.setTextSize(2);
tft.println("Spresense Sine Generator");
tft.setTextSize(2);
tft.setCursor(20, 80);
tft.printf("A0: %4d", rawFreq);
tft.setCursor(20, 110);
tft.printf("A1: %4d", rawVol);
tft.setCursor(20, 150);
tft.printf("Freq: %7.1f Hz", freq);
tft.setCursor(20, 180);
tft.printf("Vol: %3d %%", volume);
tft.setCursor(20, 210);
tft.printf("Amp: %5d", amplitude);
}
if (err_flag) {
Serial.println("[ERROR] Audio system halted!");
tft.fillScreen(ILI9341_RED);
tft.setCursor(20, 120);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.println("AUDIO SYSTEM HALTED");
player->stop(MediaPlayer::Player0);
while (1);
}
usleep(1000);
}
投稿者の人気記事





-
airpocket
さんが
2026/01/30
に
編集
をしました。
(メッセージ: 初版)
-
airpocket
さんが
2026/01/30
に
編集
をしました。
-
airpocket
さんが
2026/01/30
に
編集
をしました。
-
airpocket
さんが
2026/01/30
に
編集
をしました。
-
airpocket
さんが
2026/01/30
に
編集
をしました。
ログインしてコメントを投稿する