ひろばニュース|2026/06/19~2026/06/25号

みかん

今週のニュースです!新着などのお知らせだよ!

みかんの新着おんがく紹介①|フリーBGM『凱旋』
フリーBGM:凱旋(がいせん)
フリーBGM|ループ対応、用途:Youtube動画向け、ゲーム・アプリ向け、時間:2分41秒、BPM:116、キー:B♭m、ジャンル:あかるい、楽器:ストリングス|かっこいい凱旋をイメージした「逆転勝利!」をイメージした1曲です!エンディングで勝利の行進をするようなシーンにぴったり!
みかん

今週は1曲投稿しました!場面を豪華にしたいときはこのオーケストラ系素材を使ってみてください!

みかんの新着おんがく紹介②|今週の15秒&30秒BGMはこちら!
15秒フリーBGM:スペースオーケストラ(15秒 ver.)
フリーBGM|ループ対応、用途:Youtube動画向け、Shorts・Tiktok向け、ゲーム・アプリ向け、時間:0分15秒、BPM:174、キー:G#m、ジャンル:みらい、楽器:ストリングス、シンセサイザー|15秒BGM第55弾!近未来バトル風のオーケストラっぽい1曲です!ゲームのループBGMやSFっぽいシーンにぴったり!
30秒フリーBGM:冥府
フリーBGM|ループ対応、用途:Youtube動画向け、Shorts・Tiktok向け、ゲーム・アプリ向け、時間:0分30秒、BPM:63、キー:C#m、ジャンル:ゆったり、楽器:ピアノ、シンセサイザー|30秒BGM第38弾!かなり怖いシーンにぴったりの1曲です!冥界などのような絶望的なシーン、ホラー展開などにおすすめ!
みかん

今週も15秒&30秒BGMを追加しました!他にもあるのでぜひ見てみてください!

いろはの新着セットリスト|『洞窟』×『剣』
30秒フリーBGM:洞窟
フリーBGM|ループ対応、用途:Youtube動画向け、Shorts・Tiktok向け、ゲーム・アプリ向け、時間:0分30秒、BPM:111、キー:Bm、ジャンル:みらい、楽器:シンセサイザー|30秒BGM第32弾!洞窟での探検シーンや、TRPGでのダンジョンクエストの場面にぴったりのシンセサイザー系の一曲に仕上げました!
フリー効果音:剣
フリー効果音|ジャンル:かんきょう、バトルっぽい効果音!かっこいい剣の金属音です!
いろは

今週のセットリスト案のテーマは『ダンジョン』です。剣や魔法陣などで戦うRPGのシーンにぴったりですわね。

ことはの新着ソースコード|素材ページにて『フェードアウトON/OFF』を追加!
<div id="mikan-stem-app"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lamejs/1.2.1/lame.all.min.js"></script>

<style>
.mikan-stem-container {

    margin: 20px 0; padding: 15px;

    background: #fffaf0; border-radius: 12px; border: 1px dashed #f39800;

}

.mikan-stem-label { font-size: 0.9rem; color: #666; margin-bottom: 10px; font-weight: bold; }

.mikan-stem-controls { display: flex; flex-wrap: wrap; gap: 10px; }

.stem-btn {

    padding: 10px 18px; border: 2px solid #f39800; background: #fff; color: #f39800;

    border-radius: 25px; cursor: pointer; font-weight: bold; transition: 0.3s; outline: none;

}

.stem-btn.active { background: #f39800; color: #fff; box-shadow: 0 4px 10px rgba(243, 152, 0, 0.3); }

#mikan-vol-slider { width: 100%; cursor: pointer; accent-color: #f39800; }

.download-button.processing { opacity: 0.7; pointer-events: none; filter: grayscale(0.5); }

.fade-btn {

    padding: 10px 18px; border: 2px solid #f39800; background: #fff; color: #f39800;

    border-radius: 25px; cursor: pointer; font-weight: bold; transition: 0.3s; outline: none; margin-left: auto;

}

.fade-btn.active { background: #e67e22; border-color: #e67e22; color: #fff; box-shadow: 0 4px 10px rgba(230, 126, 34, 0.3); }

.mikan-fade-wrapper { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }

</style>
<script>
const mikanConfig = {

    artist: 'みかんのおんがくひろば',

    album: 'みかんのおんがくひろば',

    cover: 'https://mikanmusicsquare.com/wp-content/uploads/2025/10/bgmusic.png',

    stems: [

        { id: 'full',   label: 'Full Mix',             url: '' },

        { id: 'melody', label: '伴奏のみ', url: '' },

        { id: 'back',   label: 'パーカッションのみ', url: '' }

    ]

};

const mikanStem = {

    main: null,
    subAudios: {},
    dlBtn: null,
    fullUrl: "", 
    baseTitle: "", 
    currentDb: 0,
    isFadeEnabled: false,
    wasmModule: null,

async initWasm() {

        try {

            setTimeout(() => {

                if (typeof Module !== 'undefined' && Module.asm) {
                    this.wasmModule = Module;
                }

            }, 1000);

        } catch (e) {

        }

    },

    build() {

        const root = document.getElementById('mikan-stem-app');

        if (!root) return;
        let buttonsHtml = '';
        let audiosHtml = '';

        mikanConfig.stems.forEach((stem, index) => {

            const activeClass = index === 0 ? 'active' : '';

            buttonsHtml += `<button type="button" onclick="mikanStem.toggle('${stem.id}', this)" class="stem-btn ${activeClass}">${stem.label}</button>`;

            if (stem.url) {

                audiosHtml += `<audio id="stem-audio-${stem.id}" src="${stem.url}" preload="auto" style="display:none;"></audio>`;
            }
        });

root.innerHTML = `

    <div class="mikan-stem-container">

        <div class="mikan-stem-label"><i class="fas fa-sliders-h"></i> 音源切り替え</div>
        <div class="mikan-stem-controls">

            ${buttonsHtml}

            <button type="button" id="mikan-fade-toggle" onclick="mikanStem.toggleFade(this)" class="fade-btn">
                フェードアウト:OFF
            </button>
        </div>
        <hr style="border: 0; border-top: 1px dashed #f39800; margin: 20px 0; opacity: 0.3;">
        <div class="mikan-stem-label" style="margin-bottom: 10px;"><i class="fas fa-volume-down"></i> ダウンロード音量(dB)</div>
        <input type="range" id="mikan-vol-slider" min="-30" max="0" step="0.5" value="0">
        <div style="text-align: center; margin-top: 10px; font-weight: bold; color: #f39800;">
            <span id="mikan-db-display">0.0</span> dB
        </div>
    </div>
    ${audiosHtml}
`;
    },

    init() {
        this.build();
        this.initWasm();
        this.dlBtn = document.querySelector('.download-button');
        this.main = document.querySelector('.wp-audio-shortcode audio, .wp-block-audio audio');
    
        if (!this.main) return;

        const titleElement = document.querySelector('.entry-title, .wp-block-post-title');

        this.baseTitle = titleElement ? titleElement.innerText.replace(/.*フリーBGM\s*[::]\s*/, '').trim() : '名称未設定';

        this.fullUrl = this.main.src;
        mikanConfig.stems.forEach(stem => {

            if (stem.url) {

                const el = document.getElementById(`stem-audio-${stem.id}`);

                this.subAudios[stem.id] = el;

                el.muted = true;

            }
        });

        const slider = document.getElementById('mikan-vol-slider');

        const display = document.getElementById('mikan-db-display');

        slider.addEventListener('input', (e) => {

            this.currentDb = parseFloat(e.target.value);

            display.innerText = this.currentDb.toFixed(1);

            const ratio = Math.pow(10, this.currentDb / 20);

            this.main.volume = ratio;

            Object.values(this.subAudios).forEach(s => s.volume = ratio);

        });

        if (this.dlBtn) {
           this.dlBtn.addEventListener('click', (e) => {

                e.preventDefault();

                this.processDownload();

            });
        }

        const allSubs = Object.values(this.subAudios);
        this.main.addEventListener('play', () => {
            allSubs.forEach(s => { s.currentTime = this.main.currentTime; s.play(); });
        });

        this.main.addEventListener('pause', () => allSubs.forEach(s => s.pause()));

        this.main.addEventListener('seeking', () => allSubs.forEach(s => s.currentTime = this.main.currentTime));

    },

    toggleFade(btn) {
    this.isFadeEnabled = !this.isFadeEnabled;
    if (this.isFadeEnabled) {
        btn.classList.add('active');
        btn.innerHTML = 'フェードアウト:ON';
    } else {
        btn.classList.remove('active');
        btn.innerHTML = 'フェードアウト:OFF';
    }
},

    toggle(id, btn) {
        if (!this.main) return;
        this.main.muted = true;
       Object.values(this.subAudios).forEach(s => s.muted = true);

        let targetUrl = this.fullUrl;
        if (id === 'full') {
            this.main.muted = false;
        } else if (this.subAudios[id]) {
            this.subAudios[id].muted = false;
            targetUrl = this.subAudios[id].src;
        }

        if (this.dlBtn) this.dlBtn.href = targetUrl;

        document.querySelectorAll('.stem-btn').forEach(b => b.classList.remove('active'));

        if (btn) btn.classList.add('active');

    },

    async processDownload() {
        if (!this.dlBtn) return;
        const originalHtml = this.dlBtn.innerHTML;
        const url = this.dlBtn.href || this.fullUrl;

        try {
           this.dlBtn.classList.add('processing');
            this.dlBtn.innerHTML = '<i class="fas fa-cog fa-spin"></i> 生成中...';

            const ctx = new (window.AudioContext || window.webkitAudioContext)();
           const response = await fetch(url);
           const arrayBuffer = await response.arrayBuffer();
            const audioBuffer = await ctx.decodeAudioData(arrayBuffer);

            const gain = Math.pow(10, this.currentDb / 20);

            const numChannels = audioBuffer.numberOfChannels;

            const sampleRate = audioBuffer.sampleRate;

            const duration = audioBuffer.duration;

            const length = audioBuffer.length; 
          
            const mp3encoder = new lamejs.Mp3Encoder(numChannels, sampleRate, 192);
            const mp3Data = [];
            let samplesL = audioBuffer.getChannelData(0);
            let samplesR = numChannels > 1 ? audioBuffer.getChannelData(1) : samplesL;

            if (this.wasmModule) {
                const bytesPerSample = 4;
                const ptrL = this.wasmModule._malloc(length * bytesPerSample);
                const ptrR = this.wasmModule._malloc(length * bytesPerSample);
                              this.wasmModule.HEAPF32.set(samplesL, ptrL / bytesPerSample);
               this.wasmModule.HEAPF32.set(samplesR, ptrR / bytesPerSample);

                const fadeTargetTime = this.isFadeEnabled ? duration : (duration + 9999.0);
               this.wasmModule._apply_fade_and_gain(ptrL, length, sampleRate, fadeTargetTime, gain);

                if (numChannels > 1) {
                   this.wasmModule._apply_fade_and_gain(ptrR, length, sampleRate, fadeTargetTime, gain);

                }

                samplesL = new Float32Array(this.wasmModule.HEAPF32.buffer, ptrL, length);
                samplesR = numChannels > 1 ? new Float32Array(this.wasmModule.HEAPF32.buffer, ptrR, length) : samplesL;

                setTimeout(() => {
                   this.wasmModule._free(ptrL);                    this.wasmModule._free(ptrR);
                }, 100);
            } else {
                const fadeDurationSamples = sampleRate; 
                const endSampleIdx = samplesL.length;
                const startFadeIdx = endSampleIdx - fadeDurationSamples;

                for (let i = 0; i < endSampleIdx; i++) {

                    let currentGain = gain;
                                       
if (this.isFadeEnabled) {

                        if (i >= startFadeIdx) {
                            let progress = (i - startFadeIdx) / fadeDurationSamples;
                            currentGain *= (1.0 - progress);
                        }

                    }

                    samplesL[i] = samplesL[i] * currentGain;

                    if (numChannels > 1) {

                        samplesR[i] = samplesR[i] * currentGain;
                    }
                }
            }

            const block = 1152;
            for (let i = 0; i < samplesL.length; i += block) {
                const leftChunk = new Int16Array(block);
                const rightChunk = new Int16Array(block);
                for (let j = 0; j < block; j++) {
                    if (i + j < samplesL.length) {
                        leftChunk[j] = samplesL[i + j] * 32767;
                        rightChunk[j] = samplesR[i + j] * 32767;
                    }
                }

                const mp3buf = mp3encoder.encodeBuffer(leftChunk, rightChunk);

                if (mp3buf.length > 0) mp3Data.push(mp3buf);

            }
            const end = mp3encoder.flush();
            if (end.length > 0) mp3Data.push(end);

            const blob = new Blob(mp3Data, { type: 'audio/mp3' });
            const downloadUrl = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = downloadUrl;

            let baseName = url.split('/').pop().split('?')[0].replace('.mp3', '');

            const dbSuffix = (this.currentDb === 0) ? "" : `_${this.currentDb}dB`;

            const fadeSuffix = this.isFadeEnabled ? "_fadeout" : "";
            a.download = `${baseName}${dbSuffix}${fadeSuffix}.mp3`;          
            a.click();
            URL.revokeObjectURL(downloadUrl);
        } catch (e) {
            console.error(e);
            alert("MP3生成またはフェード処理に失敗しました。");

        } finally {
           this.dlBtn.classList.remove('processing');
            this.dlBtn.innerHTML = originalHtml;
        }
    }
};

window.addEventListener('load', () => mikanStem.init());

</script>
ことは

おんがく素材ページにて、素材の再生終了時間1~2秒前から『フェードアウト』処理を自動でつけるかどうかの切り替えボタンを配置しました。ぜひ一読ください。

1.素材URLから音源をブラウザにダウンロードし、Web Audio APIを使って生の波形データ(PCM)にデコード(解体)する。
2.ブラウザ内(クライアントサイド)のメモリ上で、C++(Wasm)の超高速計算によって波形データを直接書き換え、フェードアウト加工を施す。
3.加工済みの波形データを、ブラウザ内で再びMP3へエンコード(圧縮)し、サーバーを通さずにその場でダウンロード。

タイトルとURLをコピーしました