0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自作音声フォーマットを改良したい(した)

Last updated at Posted at 2026-01-16

自作音声フォーマットを改良したい!!!!!!!!!

ネタバレ

このドキュメントは、Mimi v2の仕様を決定するまでの開発者メモです
多くの課題があったv1を、より実践的に改良するための開発記録です
自分でも、なんでここまで不快かつ不愉快かつ受け入れがたい規格を作ってしまったのだろう。ごめんなさい。
見たいならこちらへ--> https://github.com/AruihaYoru/mimi
あと結構悪い記事です。いろいろとごめんなさいです。
真・最終版だけを見ることをとても推奨します。

Mimiを改良したい!!!!!!!!!

「Mimiフォーマットを利用する開発者向け」です。今のところ私だけですが。
先日、私はMimiとかいう最高に到底人間向きではなくAIでもなければ扱いずらい最悪なフォーマットを作りました。
名前はかわいいのに!!!!
DAWから逃げるって話ではある

ズバリ、あれに足りないものといえば?

そりゃ、Attack、Release、とか現代のDAWなら絶対ついてるであろう機能でしょ
あと、正弦波や鋸歯状波以外のパルスでしょ。

まって、基礎理念は?

...ごめん、すっかり忘れてた。
確かにそうだ。簡単に打てて、簡単に改造できて、軽量。
ここまでプロパティが増えてしまうともうダメだ....

Type, Pitch, Length, Start, Volume, Pan, Attack, Release

プロパティが多すぎる!

わかった。
犬かキャットかで死ぬまで喧嘩しよう!状態ですよ今。
真面目に。

V1.0の、PanとVolumeの機構をそのまま採用しよう。
だから、Attack, Releaseを省略してもよくする。
そうなると、VolumeとPanも書かなきゃいけないね。

# 4カラム。V1.0の標準。
Type, Pitch, Len, Start

# 6カラム。V1.0の完全サポート。
Type, Pitch, Len, Start, Vol, Pan

# 8カラム。V2.0はこれを目指す。
Type, Pitch, Len, Start, Vol, Pan, Atk, Rel 

んじゃあ....
う~ん...
真面目に。

新しい波形

Type

00 : 正弦波
01 : 三角波
02 : 短形波
03 : 鋸歯状波
04 : ノイズ
05 : ピンクノイズ	# よくよく考えたら00~0Fまであるのがそれっぽそうだから追加
06 : ブラウンノイズ
07 : Pulse 6.25%
08 : Pulse 12.5%
09 : Pulse 25.0%
0A : FM Growl
0B : FM Metallic
0C : Fat Super Saw
0D : 短周期ノイズ
0E : 未定
0F : 無音とか

0Eとしてこんなの作ってみた。

<!DOCTYPE html>
<button onclick="playPluck(440)">0E: Pluck (A4)</button>

<script>
const ctx = new (window.AudioContext || window.webkitAudioContext)();

function playPluck(freq) {
    const dur = 2, sr = ctx.sampleRate, N = Math.round(sr / freq);
    const buf = ctx.createBuffer(1, sr * dur, sr), d = buf.getChannelData(0);
    
    // 1. バッファの最初にノイズを充填 (弦を弾く衝撃)
    let ring = Array.from({length: N}, () => Math.random() * 2 - 1);

    // 2. 平均化しながらループ (弦の振動と減衰)
    for (let i = 0; i < d.length; i++) {
        d[i] = ring[i % N];
        ring[i % N] = (ring[i % N] + ring[(i + 1) % N]) * 0.496; // 0.5未満で減衰
    }

    const src = ctx.createBufferSource();
    src.buffer = buf;
    src.connect(ctx.destination);
    src.start();
}
</script>

....違うだろ
いいよ、載せてあげる。名前は~....Mimi-Pluck

以上。
0F作ります。

....まって、笑
ほんとに笑う
えっと、とりあえずmimi_hexっていう新しい音声フォーマットができたことだけいっときます。
mimi/mimi_hexにmp3tommimi_hexとプレイヤー置いときます

mimihexのモジュールもおいておこう。

class MimiHex {
    constructor(audioCtx, sampleRate = 8000) {
        this.ctx = audioCtx;
        this.sampleRate = sampleRate;
        this.activeNodes = new Set();
        this.output = this.ctx.createGain();
        this.output.connect(this.ctx.destination);
    }

    /**
     * @param {string} hex - 16進数文字列 ("0123456789ABCDEF...")
     * @param {number} startTime - 再生開始時間 (AudioContext.currentTime)
     */
    play(hex, startTime = this.ctx.currentTime) {
        const data = hex.replace(/[^0-9a-fA-F]/g, '');
        if (!data) return null;

        const buffer = this.ctx.createBuffer(1, data.length, this.sampleRate);
        const channelData = buffer.getChannelData(0);
        
        for (let i = 0; i < data.length; i++) {
            channelData[i] = (parseInt(data[i], 16) / 7.5) - 1.0;
        }

        const source = this.ctx.createBufferSource();
        source.buffer = buffer;
        source.connect(this.output);
        
        source.start(startTime);
        
        this.activeNodes.add(source);
        source.onended = () => {
            this.activeNodes.delete(source);
            source.disconnect();
        };

        return source;
    }

    stop() {
        this.activeNodes.forEach(node => {
            try { node.stop(); } catch(e) {}
            node.disconnect();
        });
        this.activeNodes.clear();
    }
}

んで、これを0Fに割り当てる。
カラムぶち壊して。0F, [START], 77777...(以降Mimi_hex)みたいな感じの方法でこれを呼び出すようにしよう。

真・最終版

Type

00 : 正弦波
01 : 三角波
02 : 短形波
03 : 鋸歯状波
04 : ノイズ
05 : ピンクノイズ
06 : ブラウンノイズ
07 : Pulse 6.25%
08 : Pulse 12.5%
09 : Pulse 25.0%
0A : FM Growl
0B : FM Metallic
0C : Fat Super Saw
0D : 短周期ノイズ
0E : Mimi_pluck
0F : Mimi_hex

Mimi-Pluckが浮いてる
50%ぶちこめばよかった!

カラム

# Type, Pitch, Length, Start, Volume, Attack, Release
....う~わ、Slideつけたくなってきた(「前の音から今の音へ、どれくらいの速さで繋げるか」)
わかった。

# Type, Pitch, Length, Start, Volume, Attack, Release ; Slide
これで。

エラーメッセージ

( ˶ˆ꒳ˆ˵ )  Mimi-Engine is waking up...
# きちんとmimi.v.min.jsがmimi.min.jsから読み込まれたとき

( *ˊᵕˋ)✩︎‧₊  Loading your project!
# ちゃんと受け取れた時。同時に再生。

やかましい

(。•́ - •̀。)  Mimi found a weird character...
# 数字、コンマ、セミコロン、コメントアウト以外が含まれている時。

Mimi doesn't understand your Hex language! (>_<)
# Mimi_hexに0~F以外が含まれているとき。

(இ﹏இ`。)  Mimi-Hex overflow!
[Error] Too many hexes! My buffer is crying!
# 一応用意
( ˘ω˘ )zzZ  Mimi is tired... Stopping the music.
# 再生終了

総論

人間ならV1使って
AIならmima経由でV2使って
頼む

...自分でも、なんでここまで不快かつ不愉快かつ受け入れがたい規格を作ってしまったのだろう。ごめんなさい。

推奨環境

WebAudioAPIが使えたら大体の場所で使えると思う。
...わかったよ、真面目に答える。
もしあなたが人間で、自力で使いたいならV1使いましょう。ヘッダーにv1って書いたらそれで流してくれます。
もしあなたがAIを使って作曲したいなら、mima形式で書かせたあとコンパイラしたほうがいいです。
もしあなたが人間或いはAIなら、0Fなんか使わないほうがいいです。
もしどうしても0Fが使いたいなら、素直にinput.mp3をconvert.pyに掛けたほうがいいです。

作ります

メインエントリポイント
ヘッダー解析してV1とかに飛ばす。まだV2はつけてない。

// mimi.js - メインエントリポイント
class MimiPlayer {
    constructor(fps = 24) {
        this.fps = fps;
        this.instance = null;
        this.version = null;
        this.metadata = { title: '', tempo: 120 };
        
        if (typeof MimiPlayerV1 !== 'undefined') {
            this.instance = new MimiPlayerV1(this.fps);
        }
    }

    _parseHeader(text) {
        const lines = text.split(/\r?\n/);
        const header = {
            version: "1.0",
            title: "",
            tempo: 120
        };

        for (let line of lines) {
            if (!line.startsWith('#')) break;
            
            if (line.includes('Mimi Music Format')) {
                const vMatch = line.match(/v([\d.]+)/);
                if (vMatch) header.version = vMatch[1];
            } else if (line.includes('Title:')) {
                header.title = line.split('Title:')[1].trim();
            } else if (line.includes('Tempo:')) {
                const tMatch = line.match(/(\d+)/);
                if (tMatch) header.tempo = parseInt(tMatch[1]);
            }
        }
        return header;
    }

    async load(text) {
        const info = this._parseHeader(text);
        this.version = info.version;
        this.metadata = info;

        if (this.version === "1.0") {
            if (typeof MimiPlayerV1 === 'undefined') {
                console.error("MimiPlayerV1 (mimi.v1.min.js) is not loaded.");
                return;
            }
            if (!(this.instance instanceof MimiPlayerV1)) {
                this.instance = new MimiPlayerV1(this.fps);
            }
        }
		
        return this.instance.load(text);
    }

    play(startFrame = 0) {
        if (this.instance) this.instance.play(startFrame);
    }

    stop() {
        if (this.instance) this.instance.stop();
    }

    get activeNodes() { return this.instance ? this.instance.activeNodes : new Set(); }
}

あと、mimi.hex.min.jsを忘れてますね。あれをそのまま置いとく。

./
	mimi.min.js
	mimi.hex.min.js
	mimi.v1.min.js
	mimi.v2.min.js		#これがつくってないやつ!!!
	

つくった。

完全版・V2mimi

// 完成:2026/01/16 21:50

class MimiPlayerV2 {
    constructor(fps = 24) {
        this.ctx = new (window.AudioContext || window.webkitAudioContext)();
        this.fps = fps;
        this.notes = [];
        this.activeNodes = new Set();
        this.tempo = 120;
        this.masterGain = this.ctx.createGain();
        this.masterGain.connect(this.ctx.destination);
        
        if (typeof MimiHex !== 'undefined') {
            this.hexPlayer = new MimiHex(this.ctx);
            this.hexPlayer.output.connect(this.masterGain);
        } else {
            this.hexPlayer = null;
        }

        this._waveCache = {};
        console.log("( ˶ˆ꒳ˆ˵ ) Mimi-Engine v2.0 is waking up...");
    }

    _parseHeader(text) {
        const lines = text.split('\n');
        for (let line of lines) {
            if (!line.startsWith('#')) break;
            if (line.includes('Tempo:')) {
                const tMatch = line.match(/(\d+)/);
                if (tMatch) this.tempo = parseInt(tMatch[1], 10);
            }
        }
    }

    _parsePitch(p) {
        const freq = parseFloat(p);
        if (isNaN(freq)) return 440;
        return freq < 128 ? 440 * Math.pow(2, (freq - 69) / 12) : freq;
    }

	async load(text) {
		this.stop();
		this._parseHeader(text);
		this.notes = [];
		const lines = text.split('\n');

		for (const line of lines) {
			const trimmed = line.trim();
			if (!trimmed || trimmed.startsWith('#')) continue;

			const [main, slidePart] = trimmed.split(';').map(s => s.trim());
			const cols = main.split(',').map(s => s.trim());
			
			const type = parseInt(cols[0], 16);
			
			if (type === 0x0F && cols.length < 2) continue;
			if (type !== 0x0F && cols.length < 4) continue;
			
			if (/[^0-9a-fA-F,;.\-\s]/.test(main.split('#')[0])) {
				console.warn("(。•́ - •̀。) Mimi found a weird character...");
			}

			if (type === 0x0F) {
				if (!this.hexPlayer) continue;
				const hexData = cols.slice(2).join('').replace(/[^0-9a-fA-F]/g, '');
				const duration = hexData.length / 8000;
				
				this.notes.push({
					type: 0x0F,
					start: parseFloat(cols[1]),
					rawHex: cols.slice(2).join(''),
					len: duration
				});
			} else {
				this.notes.push({
					type: type,
					pitch: this._parsePitch(cols[1]),
					len: parseFloat(cols[2]),
					start: parseFloat(cols[3]),
					vol: cols[4] ? parseFloat(cols[4]) : 0.5,
					pan: cols[5] ? parseFloat(cols[5]) : 0.0,
					atk: cols[6] ? parseFloat(cols[6]) : 0.01,
					rel: cols[7] ? parseFloat(cols[7]) : 0.1,
					slide: slidePart ? parseFloat(slidePart) : 0
				});
			}
		}
		console.log("( *ˊᵕˋ)✩︎‧₊ Loading your project!");
	}

    _getNodeTime(time) {
        return this.ctx.currentTime + time;
    }
    
    _createPeriodicWave(dutyCycle, name) {
        if (this._waveCache[name]) return this._waveCache[name];
        const n = 4096;
        const real = new Float32Array(n);
        const imag = new Float32Array(n);
        for (let i = 1; i < n; i++) {
            const sin = Math.sin(Math.PI * i * dutyCycle);
            if (sin !== 0) {
                imag[i] = (2 / (Math.PI * i)) * sin;
            }
        }
        const wave = this.ctx.createPeriodicWave(real, imag, { disableNormalization: false });
        this._waveCache[name] = wave;
        return wave;
    }

    _createNoiseBuffer(type, duration) {
        const len = this.ctx.sampleRate * duration;
        const buffer = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
        const data = buffer.getChannelData(0);
        let lastOut = 0;
        let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;

        for (let i = 0; i < len; i++) {
            switch (type) {
                case 0x04: // White
                    data[i] = Math.random() * 2 - 1;
                    break;
                case 0x05: // Pink
                    const white = Math.random() * 2 - 1;
                    b0 = 0.99886 * b0 + white * 0.0555179;
                    b1 = 0.99332 * b1 + white * 0.0750759;
                    b2 = 0.96900 * b2 + white * 0.1538520;
                    b3 = 0.86650 * b3 + white * 0.3104856;
                    b4 = 0.55000 * b4 + white * 0.5329522;
                    b5 = -0.7616 * b5 - white * 0.0168980;
                    data[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362) * 0.11;
                    b6 = white * 0.115926;
                    break;
                case 0x06: // Brown
                    const r = Math.random() * 2 - 1;
                    data[i] = (lastOut + (0.02 * r)) / 1.02;
                    lastOut = data[i];
                    data[i] *= 3.5;
                    break;
            }
        }
        return buffer;
    }

    play(startFrame = 0) {
        if (this.ctx.state === 'suspended') this.ctx.resume();
        const frameDuration = 60 / (this.tempo * (this.fps / 6));
        const startTimeOffset = startFrame * (1 / this.fps);

		// console.log("=== Play Debug ===");
		// console.log("frameDuration:", frameDuration);
		// console.log("startTimeOffset:", startTimeOffset);
		// Note: このバグ(Mimi_hexが再生時間に加算されない)を解決するにあたって、奮闘していただいたClaude Opus 4.5を筆頭としたAIたちに、最大限の敬意と感謝を払います。
			
        this.notes.forEach(n => {
            const scheduledStart = this._getNodeTime(n.start * frameDuration - startTimeOffset);
            if (scheduledStart < this.ctx.currentTime) return;

            if (n.type === 0x0F) {
                if(this.hexPlayer.play(n.rawHex, scheduledStart) === null){
                    console.error("Mimi doesn't understand your Hex language! (>_<)");
                }
                return;
            }

            const source = this._createSource(n, scheduledStart);
            if (!source) return;

            const envelope = this._createEnvelope(n, scheduledStart);
            const panner = this.ctx.createStereoPanner();
            panner.pan.setValueAtTime(n.pan, scheduledStart);
            
            source.connect(envelope).connect(panner).connect(this.masterGain);
        });

        const totalDuration = Math.max(0, ...this.notes.map(n => (n.start * frameDuration) + (n.len || 0))) * 1000;
        setTimeout(() => console.log("( ˘ω˘ )zzZ Mimi is tired..."), totalDuration);
    }
    
    _createSource(n, start) {
        let source;
        const duration = n.len + n.rel;

        switch (n.type) {
            case 0x00: case 0x01: case 0x02: case 0x03:
                source = this.ctx.createOscillator();
                source.type = ['sine', 'triangle', 'square', 'sawtooth'][n.type];
                source.frequency.setValueAtTime(n.pitch, start);
                break;
            case 0x04: case 0x05: case 0x06:
                source = this.ctx.createBufferSource();
                source.buffer = this._createNoiseBuffer(n.type, duration);
                source.loop = true;
                break;
            case 0x07: source = this._createPulseOsc(n, 0.0625, start); break;
            case 0x08: source = this._createPulseOsc(n, 0.125, start); break;
            case 0x09: source = this._createPulseOsc(n, 0.25, start); break;
            case 0x0A: source = this._createFMSynth(n, 'growl', start); break;
            case 0x0B: source = this._createFMSynth(n, 'metallic', start); break;
            case 0x0C: source = this._createSuperSaw(n, start); break;
            case 0x0D: source = this._createShortNoise(n, start); break;
            case 0x0E: source = this._createPluck(n, start); break;
            default: return null;
        }

        if (n.slide > 0 && source.frequency) {
            source.frequency.linearRampToValueAtTime(this._parsePitch(n.pitch), start + n.slide);
        }

        if (typeof source.start === 'function') {
            source.start(start);
            source.stop(start + duration);
            this.activeNodes.add(source);
            source.onended = () => this.activeNodes.delete(source);
        }
        return source;
    }

    _createEnvelope(n, start) {
        const gain = this.ctx.createGain();
        const vol = Math.max(0, Math.min(1, n.vol));
        const atk = Math.max(0.001, n.atk);
        const rel = Math.max(0.001, n.rel);
        const len = Math.max(0, n.len);
        
        gain.gain.setValueAtTime(0, start);
        gain.gain.linearRampToValueAtTime(vol, start + atk);
        if (len > atk + rel) {
             gain.gain.setValueAtTime(vol, start + len - rel);
        }
        gain.gain.linearRampToValueAtTime(0, start + len);
        return gain;
    }
    
    _createPulseOsc(n, duty, start) {
        const osc = this.ctx.createOscillator();
        osc.setPeriodicWave(this._createPeriodicWave(duty, `pulse${duty*100}`));
        osc.frequency.setValueAtTime(n.pitch, start);
        return osc;
    }

    _createFMSynth(n, mode, start) {
        const carrier = this.ctx.createOscillator();
        const modulator = this.ctx.createOscillator();
        const modGain = this.ctx.createGain();
        
        const config = {
            growl: { modRatio: 0.5, modIndex: 1000 },
            metallic: { modRatio: 1.4, modIndex: 800 }
        }[mode];

        modulator.frequency.setValueAtTime(n.pitch * config.modRatio, start);
        modGain.gain.setValueAtTime(config.modIndex, start);
        carrier.frequency.setValueAtTime(n.pitch, start);

        modulator.connect(modGain).connect(carrier.frequency);
        modulator.start(start);
        modulator.stop(start + n.len + n.rel);
        this.activeNodes.add(modulator);
        return carrier;
    }
    
    _createSuperSaw(n, start) {
        const master = this.ctx.createGain();
        for (let i = 0; i < 7; i++) {
            const osc = this.ctx.createOscillator();
            osc.type = 'sawtooth';
            const detune = (i - 3) * 10;
            osc.frequency.setValueAtTime(n.pitch, start);
            osc.detune.setValueAtTime(detune, start);
            const gain = this.ctx.createGain();
            gain.gain.value = i === 3 ? 1.0 : 0.6;
            osc.connect(gain).connect(master);
            osc.start(start);
            osc.stop(start + n.len + n.rel);
            this.activeNodes.add(osc);
        }
        return master;
    }
    
    _createShortNoise(n, start) {
        const len = 16;
        const buffer = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
        const data = buffer.getChannelData(0);
        for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
        const source = this.ctx.createBufferSource();
        source.buffer = buffer;
        source.loop = true;
        source.playbackRate.setValueAtTime(n.pitch / 220, start); // A2=220Hz as base
        return source;
    }
    
    _createPluck(n, start) {
        const dur = n.len, sr = this.ctx.sampleRate, N = Math.round(sr / n.pitch);
        const buf = this.ctx.createBuffer(1, sr * dur, sr), d = buf.getChannelData(0);
        let ring = Array.from({length: N}, () => Math.random() * 2 - 1);
        for (let i = 0; i < d.length; i++) {
            d[i] = ring[i % N];
            ring[i % N] = (ring[i % N] + ring[(i + 1) % N]) * 0.496;
        }
        const src = this.ctx.createBufferSource();
        src.buffer = buf;
        return src;
    }

    stop() {
        this.activeNodes.forEach(node => {
            try { node.stop(0); } catch (e) {}
            if (node.disconnect) node.disconnect();
        });
        this.activeNodes.clear();
        if (this.hexPlayer) this.hexPlayer.stop();
    }
}


あと、mimi.hex.min.jsのthis.output.connect(this.ctx.destination);がいりませんでしたね。
これにあわせてLLMimiを少し変えました。以上です
見たいならこちらへ--> https://github.com/AruihaYoru/mimi

ありがとう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?