Simple Tools Hub - Simple Online Tools

general

音声変換ツール完全ガイド|MP3・WAV・FLAC対応の高品質オーディオ処理技術

音声フォーマット変換の基礎、ビットレート最適化、サンプリングレート変換、コーデック選択、バッチ処理、メタデータ保持まで、プロ級の音声処理技術を4500字で徹底解説

18 min read
音声変換ツール完全ガイド|MP3・WAV・FLAC対応の高品質オーディオ処理技術

音声変換ツール完全ガイド

はじめに:デジタル音声の最適化戦略

デジタル音声の世界では、用途に応じた適切なフォーマット選択が品質とファイルサイズのバランスを左右します。ストリーミング配信、ポッドキャスト制作、音楽アーカイブなど、それぞれのニーズに最適な音声フォーマットと変換技術があります。本記事では、音声変換の技術的側面から実践的な活用方法まで、包括的に解説します。

第1章:音声フォーマットの技術詳細

1.1 主要フォーマットの特性比較

各フォーマットの技術仕様

const audioFormats = {
  WAV: {
    compression: 'none',
    type: 'lossless',
    bitDepth: [8, 16, 24, 32],
    sampleRates: [8000, 16000, 22050, 44100, 48000, 96000, 192000],
    channels: [1, 2, 5.1, 7.1],
    fileSize: 'large',
    quality: 'perfect',
    useCase: 'マスタリング、編集作業',
    pros: '無圧縮、完全な音質',
    cons: 'ファイルサイズが大きい'
  },

  FLAC: {
    compression: 'lossless',
    type: 'lossless',
    compressionRatio: 0.5, // 約50%圧縮
    bitDepth: [8, 16, 24],
    sampleRates: [8000, 16000, 22050, 44100, 48000, 88200, 96000, 192000],
    quality: 'perfect',
    useCase: '音楽アーカイブ、高品質配信',
    pros: '可逆圧縮、メタデータ対応',
    cons: '処理負荷が高い'
  },

  MP3: {
    compression: 'lossy',
    type: 'lossy',
    bitrates: [32, 64, 128, 192, 256, 320], // kbps
    vbr: true, // Variable Bit Rate対応
    sampleRates: [8000, 16000, 22050, 32000, 44100, 48000],
    quality: 'good',
    useCase: 'ストリーミング、ポータブル',
    pros: '高圧縮率、広い互換性',
    cons: '音質劣化あり'
  },

  AAC: {
    compression: 'lossy',
    type: 'lossy',
    profiles: ['AAC-LC', 'HE-AAC', 'HE-AAC v2'],
    bitrates: [16, 32, 64, 96, 128, 256, 320],
    quality: 'very good',
    useCase: 'ストリーミング、Apple製品',
    pros: 'MP3より高効率、低ビットレートで高品質',
    cons: 'ライセンス制約'
  },

  OGG: {
    compression: 'lossy',
    codec: 'Vorbis',
    type: 'lossy',
    bitrates: [45, 64, 96, 128, 160, 192, 224, 256, 320, 500],
    quality: 'very good',
    useCase: 'ゲーム、Web配信',
    pros: 'オープンソース、高品質',
    cons: 'Apple製品での互換性問題'
  }
};

// ビットレート計算
function calculateBitrate(sampleRate, bitDepth, channels) {
  return sampleRate * bitDepth * channels;
}

// ファイルサイズ予測
function estimateFileSize(duration, bitrate) {
  return (bitrate * duration) / 8; // bytes
}

1.2 サンプリングレートとビット深度

音質への影響と選択基準

class AudioQualityManager {
  // ナイキスト定理に基づく最適サンプリングレート
  getOptimalSampleRate(maxFrequency) {
    return maxFrequency * 2;
  }

  // 用途別推奨設定
  getRecommendedSettings(useCase) {
    const settings = {
      'voice': {
        sampleRate: 16000,
        bitDepth: 16,
        channels: 1,
        format: 'mp3',
        bitrate: 64
      },
      'podcast': {
        sampleRate: 44100,
        bitDepth: 16,
        channels: 1,
        format: 'mp3',
        bitrate: 128
      },
      'music-streaming': {
        sampleRate: 44100,
        bitDepth: 16,
        channels: 2,
        format: 'aac',
        bitrate: 256
      },
      'music-archive': {
        sampleRate: 96000,
        bitDepth: 24,
        channels: 2,
        format: 'flac',
        bitrate: null // lossless
      },
      'professional': {
        sampleRate: 192000,
        bitDepth: 32,
        channels: 2,
        format: 'wav',
        bitrate: null // uncompressed
      }
    };

    return settings[useCase] || settings['music-streaming'];
  }

  // リサンプリング品質設定
  getResamplingFilter(fromRate, toRate) {
    const ratio = toRate / fromRate;

    if (ratio > 1) {
      // アップサンプリング
      return {
        type: 'sinc',
        windowFunction: 'blackman-harris',
        taps: 256,
        cutoff: 0.9
      };
    } else {
      // ダウンサンプリング
      return {
        type: 'sinc',
        windowFunction: 'kaiser',
        beta: 8.6,
        taps: 512,
        cutoff: 0.95 * ratio
      };
    }
  }
}

第2章:高品質変換処理の実装

2.1 FFmpegを使用した変換

Node.jsでの音声変換実装

const ffmpeg = require('fluent-ffmpeg');
const fs = require('fs').promises;
const path = require('path');

class AudioConverter {
  constructor(options = {}) {
    this.ffmpegPath = options.ffmpegPath || 'ffmpeg';
    this.threads = options.threads || 4;
    this.normalize = options.normalize || false;
  }

  async convert(inputPath, outputPath, options = {}) {
    const {
      format = 'mp3',
      bitrate = '192k',
      sampleRate = 44100,
      channels = 2,
      codec = this.getDefaultCodec(format),
      vbr = false,
      quality = 2, // VBR quality (0-9, 0=best)
      metadata = true
    } = options;

    return new Promise((resolve, reject) => {
      let command = ffmpeg(inputPath)
        .audioCodec(codec)
        .audioFrequency(sampleRate)
        .audioChannels(channels);

      // ビットレート設定
      if (vbr && format === 'mp3') {
        command = command.audioQuality(quality);
      } else {
        command = command.audioBitrate(bitrate);
      }

      // ノーマライズ処理
      if (this.normalize) {
        command = command.audioFilters([
          'loudnorm=I=-16:LRA=11:TP=-1.5'
        ]);
      }

      // 追加フィルター
      if (options.filters) {
        command = command.audioFilters(options.filters);
      }

      // メタデータ保持
      if (metadata) {
        command = command.outputOptions('-map_metadata 0');
      }

      // 実行
      command
        .output(outputPath)
        .on('start', (commandLine) => {
          console.log('FFmpeg command:', commandLine);
        })
        .on('progress', (progress) => {
          if (options.onProgress) {
            options.onProgress(progress);
          }
        })
        .on('end', () => {
          resolve({
            success: true,
            output: outputPath,
            format: format
          });
        })
        .on('error', (err) => {
          reject(err);
        })
        .run();
    });
  }

  getDefaultCodec(format) {
    const codecs = {
      'mp3': 'libmp3lame',
      'aac': 'aac',
      'ogg': 'libvorbis',
      'flac': 'flac',
      'wav': 'pcm_s16le',
      'opus': 'libopus',
      'm4a': 'aac',
      'wma': 'wmav2'
    };

    return codecs[format] || 'copy';
  }

  // バッチ変換
  async batchConvert(files, outputDir, options = {}) {
    const results = [];
    const { parallel = 2 } = options;

    // 並列処理の制御
    const chunks = this.chunkArray(files, parallel);

    for (const chunk of chunks) {
      const promises = chunk.map(async (file) => {
        const outputName = path.basename(file.input, path.extname(file.input));
        const outputPath = path.join(outputDir, `${outputName}.${options.format || 'mp3'}`);

        try {
          const result = await this.convert(file.input, outputPath, {
            ...options,
            ...file.options
          });
          return { ...result, original: file.input };
        } catch (error) {
          return {
            success: false,
            original: file.input,
            error: error.message
          };
        }
      });

      const chunkResults = await Promise.all(promises);
      results.push(...chunkResults);
    }

    return this.generateReport(results);
  }

  chunkArray(array, size) {
    const chunks = [];
    for (let i = 0; i < array.length; i += size) {
      chunks.push(array.slice(i, i + size));
    }
    return chunks;
  }

  generateReport(results) {
    const successful = results.filter(r => r.success);
    const failed = results.filter(r => !r.success);

    return {
      total: results.length,
      successful: successful.length,
      failed: failed.length,
      results: results,
      summary: {
        formats: [...new Set(successful.map(r => r.format))],
        totalProcessingTime: results.reduce((acc, r) => acc + (r.processingTime || 0), 0)
      }
    };
  }
}

2.2 高度な音声処理

エフェクトとフィルター適用

class AudioProcessor {
  // イコライザー処理
  applyEqualizer(inputPath, outputPath, bands) {
    const filters = bands.map(band => {
      return `equalizer=f=${band.frequency}:t=q:w=${band.width}:g=${band.gain}`;
    }).join(',');

    return ffmpeg(inputPath)
      .audioFilters(filters)
      .output(outputPath)
      .run();
  }

  // ノイズ除去
  async removeNoise(inputPath, outputPath, options = {}) {
    const {
      noiseProfile = null,
      sensitivity = 0.21,
      noiseReduction = 0.3
    } = options;

    // ステップ1: ノイズプロファイルの生成
    if (!noiseProfile) {
      await this.generateNoiseProfile(inputPath);
    }

    // ステップ2: ノイズ除去
    return ffmpeg(inputPath)
      .audioFilters([
        `highpass=f=100`,
        `lowpass=f=8000`,
        `afftdn=nr=${noiseReduction}:nf=-20`
      ])
      .output(outputPath)
      .run();
  }

  // コンプレッサー
  applyCompressor(inputPath, outputPath, options = {}) {
    const {
      threshold = -20,  // dB
      ratio = 4,         // 4:1
      attack = 5,        // ms
      release = 100,     // ms
      makeup = 2         // dB
    } = options;

    return ffmpeg(inputPath)
      .audioFilters([
        `acompressor=threshold=${threshold}dB:ratio=${ratio}:attack=${attack}:release=${release}:makeup=${makeup}dB`
      ])
      .output(outputPath)
      .run();
  }

  // リバーブ追加
  addReverb(inputPath, outputPath, options = {}) {
    const {
      roomSize = 0.5,
      damping = 0.5,
      wetLevel = 0.3,
      dryLevel = 0.7
    } = options;

    // Freeverb アルゴリズム
    return ffmpeg(inputPath)
      .complexFilter([
        `[0:a]aecho=0.8:0.9:40:0.5[wet]`,
        `[0:a]volume=${dryLevel}[dry]`,
        `[wet]volume=${wetLevel}[wet2]`,
        `[dry][wet2]amix=inputs=2[out]`
      ])
      .outputOptions(['-map', '[out]'])
      .output(outputPath)
      .run();
  }

  // ピッチシフト
  changePitch(inputPath, outputPath, semitones) {
    const pitchFactor = Math.pow(2, semitones / 12);

    return ffmpeg(inputPath)
      .audioFilters([
        `asetrate=44100*${pitchFactor},aresample=44100`
      ])
      .output(outputPath)
      .run();
  }

  // テンポ変更(ピッチ維持)
  changeTempo(inputPath, outputPath, factor) {
    return ffmpeg(inputPath)
      .audioFilters([
        `atempo=${factor}`
      ])
      .output(outputPath)
      .run();
  }
}

2.3 ストリーミング最適化

アダプティブビットレート対応

class StreamingOptimizer {
  // HLS用セグメント生成
  async generateHLS(inputPath, outputDir, options = {}) {
    const {
      segmentDuration = 10,
      variants = [
        { bitrate: 64, name: 'low' },
        { bitrate: 128, name: 'medium' },
        { bitrate: 256, name: 'high' }
      ]
    } = options;

    const masterPlaylist = ['#EXTM3U', '#EXT-X-VERSION:3'];

    for (const variant of variants) {
      const variantDir = path.join(outputDir, variant.name);
      await fs.mkdir(variantDir, { recursive: true });

      // 各ビットレートのストリーム生成
      await new Promise((resolve, reject) => {
        ffmpeg(inputPath)
          .audioBitrate(`${variant.bitrate}k`)
          .audioCodec('aac')
          .outputOptions([
            '-hls_time', segmentDuration,
            '-hls_playlist_type', 'vod',
            '-hls_segment_filename', path.join(variantDir, 'segment_%03d.ts')
          ])
          .output(path.join(variantDir, 'playlist.m3u8'))
          .on('end', resolve)
          .on('error', reject)
          .run();
      });

      // マスタープレイリストに追加
      masterPlaylist.push(
        `#EXT-X-STREAM-INF:BANDWIDTH=${variant.bitrate * 1000},CODECS="mp4a.40.2"`,
        `${variant.name}/playlist.m3u8`
      );
    }

    // マスタープレイリスト保存
    await fs.writeFile(
      path.join(outputDir, 'master.m3u8'),
      masterPlaylist.join('\n')
    );
  }

  // DASH用マニフェスト生成
  async generateDASH(inputPath, outputDir, options = {}) {
    const variants = options.variants || [64, 128, 256];

    const dashCommand = ffmpeg(inputPath);

    variants.forEach((bitrate, index) => {
      dashCommand
        .output(`${outputDir}/audio_${bitrate}k.mp4`)
        .audioBitrate(`${bitrate}k`)
        .audioCodec('aac');
    });

    return dashCommand
      .outputOptions([
        '-dash', '1',
        '-dash_segment_type', 'mp4'
      ])
      .run();
  }
}

第3章:メタデータ処理

3.1 ID3タグの管理

メタデータの読み取りと編集

const NodeID3 = require('node-id3');
const musicMetadata = require('music-metadata');

class MetadataManager {
  // メタデータ読み取り
  async readMetadata(filePath) {
    try {
      const metadata = await musicMetadata.parseFile(filePath);

      return {
        format: metadata.format,
        common: {
          title: metadata.common.title,
          artist: metadata.common.artist,
          album: metadata.common.album,
          year: metadata.common.year,
          genre: metadata.common.genre,
          track: metadata.common.track,
          albumArt: metadata.common.picture
        },
        technical: {
          duration: metadata.format.duration,
          bitrate: metadata.format.bitrate,
          sampleRate: metadata.format.sampleRate,
          channels: metadata.format.numberOfChannels,
          codec: metadata.format.codec,
          lossless: metadata.format.lossless
        }
      };
    } catch (error) {
      console.error('Metadata read error:', error);
      return null;
    }
  }

  // ID3タグ書き込み
  async writeID3Tags(filePath, tags) {
    const id3Tags = {
      title: tags.title,
      artist: tags.artist,
      album: tags.album,
      year: tags.year,
      comment: tags.comment,
      track: tags.track,
      genre: tags.genre
    };

    // アルバムアート処理
    if (tags.albumArt) {
      id3Tags.image = {
        mime: tags.albumArt.mime || 'image/jpeg',
        type: {
          id: 3,
          name: 'Cover (front)'
        },
        description: 'Album cover',
        imageBuffer: tags.albumArt.buffer
      };
    }

    // 歌詞追加
    if (tags.lyrics) {
      id3Tags.unsynchronisedLyrics = {
        language: 'eng',
        text: tags.lyrics
      };
    }

    return NodeID3.write(id3Tags, filePath);
  }

  // メタデータのコピー
  async copyMetadata(sourcePath, targetPath) {
    const sourceMetadata = await this.readMetadata(sourcePath);
    if (!sourceMetadata) return false;

    return this.writeID3Tags(targetPath, sourceMetadata.common);
  }

  // アルバムアート抽出
  async extractAlbumArt(audioPath, outputPath) {
    const metadata = await musicMetadata.parseFile(audioPath);

    if (metadata.common.picture && metadata.common.picture.length > 0) {
      const picture = metadata.common.picture[0];
      await fs.writeFile(outputPath, picture.data);
      return {
        success: true,
        format: picture.format,
        size: picture.data.length
      };
    }

    return { success: false, error: 'No album art found' };
  }

  // バッチメタデータ更新
  async batchUpdateMetadata(files, commonTags = {}) {
    const results = [];

    for (const file of files) {
      try {
        const existingMetadata = await this.readMetadata(file);
        const updatedTags = {
          ...existingMetadata.common,
          ...commonTags,
          ...file.specificTags
        };

        await this.writeID3Tags(file, updatedTags);
        results.push({
          file,
          success: true
        });
      } catch (error) {
        results.push({
          file,
          success: false,
          error: error.message
        });
      }
    }

    return results;
  }
}

第4章:Web Audio APIでの処理

4.1 ブラウザでの音声処理

リアルタイム音声処理

class WebAudioProcessor {
  constructor() {
    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
    this.nodes = {};
  }

  // 音声ファイルの読み込みと変換
  async loadAndProcess(file, effects = []) {
    const arrayBuffer = await file.arrayBuffer();
    const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);

    // オフライン処理用コンテキスト
    const offlineContext = new OfflineAudioContext(
      audioBuffer.numberOfChannels,
      audioBuffer.length,
      audioBuffer.sampleRate
    );

    // ソースノード
    const source = offlineContext.createBufferSource();
    source.buffer = audioBuffer;

    // エフェクトチェーン構築
    let currentNode = source;
    for (const effect of effects) {
      const effectNode = this.createEffectNode(offlineContext, effect);
      currentNode.connect(effectNode);
      currentNode = effectNode;
    }

    // 出力に接続
    currentNode.connect(offlineContext.destination);

    // 処理開始
    source.start(0);
    const renderedBuffer = await offlineContext.startRendering();

    return renderedBuffer;
  }

  createEffectNode(context, effect) {
    switch (effect.type) {
      case 'gain':
        const gainNode = context.createGain();
        gainNode.gain.value = effect.value;
        return gainNode;

      case 'filter':
        const filterNode = context.createBiquadFilter();
        filterNode.type = effect.filterType || 'lowpass';
        filterNode.frequency.value = effect.frequency || 1000;
        filterNode.Q.value = effect.q || 1;
        return filterNode;

      case 'compressor':
        const compressor = context.createDynamicsCompressor();
        compressor.threshold.value = effect.threshold || -24;
        compressor.knee.value = effect.knee || 30;
        compressor.ratio.value = effect.ratio || 12;
        compressor.attack.value = effect.attack || 0.003;
        compressor.release.value = effect.release || 0.25;
        return compressor;

      case 'convolver':
        const convolver = context.createConvolver();
        convolver.buffer = effect.impulseResponse;
        return convolver;

      case 'delay':
        const delay = context.createDelay();
        delay.delayTime.value = effect.delayTime || 0.5;
        return delay;

      default:
        // パススルー
        const passthrough = context.createGain();
        passthrough.gain.value = 1;
        return passthrough;
    }
  }

  // WAVエクスポート
  exportWAV(audioBuffer) {
    const length = audioBuffer.length * audioBuffer.numberOfChannels * 2 + 44;
    const buffer = new ArrayBuffer(length);
    const view = new DataView(buffer);
    const channels = [];
    let offset = 0;
    let pos = 0;

    // WAVヘッダー書き込み
    const setUint16 = (data) => {
      view.setUint16(pos, data, true);
      pos += 2;
    };

    const setUint32 = (data) => {
      view.setUint32(pos, data, true);
      pos += 4;
    };

    // RIFF identifier
    setUint32(0x46464952);
    // file length
    setUint32(length - 8);
    // WAVE identifier
    setUint32(0x45564157);
    // fmt chunk identifier
    setUint32(0x20746d66);
    // chunk length
    setUint32(16);
    // sample format (PCM)
    setUint16(1);
    // channel count
    setUint16(audioBuffer.numberOfChannels);
    // sample rate
    setUint32(audioBuffer.sampleRate);
    // byte rate
    setUint32(audioBuffer.sampleRate * 2 * audioBuffer.numberOfChannels);
    // block align
    setUint16(audioBuffer.numberOfChannels * 2);
    // bits per sample
    setUint16(16);
    // data chunk identifier
    setUint32(0x61746164);
    // data chunk length
    setUint32(length - pos - 4);

    // 音声データ書き込み
    const channels = [];
    for (let i = 0; i < audioBuffer.numberOfChannels; i++) {
      channels.push(audioBuffer.getChannelData(i));
    }

    let offset = pos;
    for (let i = 0; i < audioBuffer.length; i++) {
      for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
        const sample = Math.max(-1, Math.min(1, channels[channel][i]));
        view.setInt16(offset, sample * 0x7FFF, true);
        offset += 2;
      }
    }

    return new Blob([buffer], { type: 'audio/wav' });
  }

  // MP3エンコーディング(lamejs使用)
  async exportMP3(audioBuffer, bitrate = 128) {
    const lamejs = await import('lamejs');
    const mp3encoder = new lamejs.Mp3Encoder(
      audioBuffer.numberOfChannels,
      audioBuffer.sampleRate,
      bitrate
    );

    const samples = audioBuffer.length;
    const leftChannel = audioBuffer.getChannelData(0);
    const rightChannel = audioBuffer.numberOfChannels > 1
      ? audioBuffer.getChannelData(1)
      : leftChannel;

    const sampleBlockSize = 1152;
    const mp3Data = [];

    for (let i = 0; i < samples; i += sampleBlockSize) {
      const leftChunk = leftChannel.subarray(i, i + sampleBlockSize);
      const rightChunk = rightChannel.subarray(i, i + sampleBlockSize);

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

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

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

    return new Blob(mp3Data, { type: 'audio/mp3' });
  }

  convertFloat32ToInt16(buffer) {
    const l = buffer.length;
    const buf = new Int16Array(l);

    for (let i = 0; i < l; i++) {
      buf[i] = Math.min(1, buffer[i]) * 0x7FFF;
    }

    return buf;
  }
}

第5章:品質評価とトラブルシューティング

5.1 音質評価メトリクス

客観的音質評価

class AudioQualityAnalyzer {
  // PESQ (Perceptual Evaluation of Speech Quality) シミュレーション
  calculatePESQ(original, processed) {
    // 簡易的なPESQスコア計算
    const snr = this.calculateSNR(original, processed);
    const thd = this.calculateTHD(processed);

    // PESQスコアの近似計算
    let pesq = 4.5; // 最大スコア

    // SNRに基づく減点
    if (snr < 30) pesq -= (30 - snr) * 0.05;

    // THDに基づく減点
    if (thd > 0.01) pesq -= thd * 10;

    return Math.max(1, Math.min(4.5, pesq));
  }

  // SNR (Signal-to-Noise Ratio) 計算
  calculateSNR(signal, noisySignal) {
    let signalPower = 0;
    let noisePower = 0;

    for (let i = 0; i < signal.length; i++) {
      signalPower += signal[i] ** 2;
      const noise = noisySignal[i] - signal[i];
      noisePower += noise ** 2;
    }

    if (noisePower === 0) return Infinity;

    return 10 * Math.log10(signalPower / noisePower);
  }

  // THD (Total Harmonic Distortion) 計算
  calculateTHD(signal) {
    const fft = this.performFFT(signal);
    const magnitudes = fft.map(c => Math.sqrt(c.real ** 2 + c.imag ** 2));

    // 基本周波数を見つける
    let fundamentalIdx = 0;
    let maxMag = 0;
    for (let i = 1; i < magnitudes.length / 2; i++) {
      if (magnitudes[i] > maxMag) {
        maxMag = magnitudes[i];
        fundamentalIdx = i;
      }
    }

    // 高調波成分の合計
    let harmonicSum = 0;
    for (let n = 2; n <= 5; n++) {
      const harmonicIdx = fundamentalIdx * n;
      if (harmonicIdx < magnitudes.length / 2) {
        harmonicSum += magnitudes[harmonicIdx] ** 2;
      }
    }

    return Math.sqrt(harmonicSum) / magnitudes[fundamentalIdx];
  }

  // FFT実装(簡易版)
  performFFT(signal) {
    // 実際の実装ではFFTライブラリを使用
    const N = signal.length;
    const fft = [];

    for (let k = 0; k < N; k++) {
      let real = 0;
      let imag = 0;

      for (let n = 0; n < N; n++) {
        const angle = -2 * Math.PI * k * n / N;
        real += signal[n] * Math.cos(angle);
        imag += signal[n] * Math.sin(angle);
      }

      fft.push({ real, imag });
    }

    return fft;
  }

  // スペクトログラム生成
  generateSpectrogram(audioBuffer, windowSize = 2048, hopSize = 512) {
    const data = audioBuffer.getChannelData(0);
    const spectrogram = [];

    for (let i = 0; i < data.length - windowSize; i += hopSize) {
      const window = data.slice(i, i + windowSize);

      // ハミング窓を適用
      const windowed = window.map((sample, idx) => {
        const hammingCoeff = 0.54 - 0.46 * Math.cos(2 * Math.PI * idx / (windowSize - 1));
        return sample * hammingCoeff;
      });

      const fft = this.performFFT(windowed);
      const magnitudes = fft.slice(0, windowSize / 2).map(c =>
        Math.sqrt(c.real ** 2 + c.imag ** 2)
      );

      spectrogram.push(magnitudes);
    }

    return spectrogram;
  }
}

5.2 一般的な問題と解決策

トラブルシューティングガイド

class AudioTroubleshooter {
  diagnoseIssue(symptoms) {
    const issues = {
      'clipping': {
        symptoms: ['歪み', 'パチパチ音'],
        cause: '音量レベルが高すぎる',
        solution: 'ゲインを下げる、リミッターを適用'
      },
      'aliasing': {
        symptoms: ['金属的な音', '高周波ノイズ'],
        cause: 'サンプリングレート変換の問題',
        solution: 'アンチエイリアスフィルターを適用'
      },
      'phase_issues': {
        symptoms: ['音が薄い', 'ステレオ感の喪失'],
        cause: '位相のずれ',
        solution: '位相補正、モノラル変換'
      },
      'encoding_artifacts': {
        symptoms: ['水中音', 'プリエコー'],
        cause: '低ビットレートエンコーディング',
        solution: 'ビットレートを上げる、別のコーデックを使用'
      }
    };

    return issues[symptoms] || {
      symptoms: symptoms,
      cause: '不明',
      solution: '詳細な分析が必要'
    };
  }

  // 自動修復
  async autoFix(audioPath, issues) {
    const fixes = [];

    if (issues.includes('clipping')) {
      fixes.push('dynaudnorm=f=150:g=15');
    }

    if (issues.includes('noise')) {
      fixes.push('afftdn=nr=20:nf=-20');
    }

    if (issues.includes('low_volume')) {
      fixes.push('loudnorm=I=-16:TP=-1.5:LRA=11');
    }

    if (fixes.length === 0) {
      return { success: false, message: 'No automatic fixes available' };
    }

    const outputPath = audioPath.replace(/\.[^.]+$/, '_fixed.wav');

    return ffmpeg(audioPath)
      .audioFilters(fixes)
      .output(outputPath)
      .run();
  }
}

Security and Privacy

All processing is done within your browser, and no data is sent externally. You can safely use it with personal or confidential information.

Troubleshooting

Common Issues

  • Not working: Clear browser cache and reload
  • Slow processing: Check file size (recommended under 20MB)
  • Unexpected results: Verify input format and settings

If issues persist, update your browser to the latest version or try a different browser.

まとめ:プロフェッショナルな音声変換戦略

音声変換は単なるフォーマット変更以上の技術とノウハウが必要です。以下のポイントを押さえることで、高品質な音声処理を実現できます:

  1. 適切なフォーマット選択:用途に応じた最適なコーデックとビットレート
  2. 品質とサイズのバランス:圧縮率と音質の最適化
  3. メタデータの保持:ID3タグとアルバムアートの管理
  4. ストリーミング対応:アダプティブビットレートの実装
  5. 品質評価:客観的メトリクスによる検証

i4uの音声変換ツールを活用することで、簡単に高品質な音声変換を実行できます。

カテゴリ別ツール

他のツールもご覧ください:

関連ツール