SoundPlayerクラスには再生される音声のボリュームを設定・変更するプロパティやメソッドが用意されていない。 一方、SoundPlayerは任意のStreamから音声を読み込み再生することができる。 そこで、音声の読み込みと同時にボリュームの変更を行うようにしたStreamを作成し、それを音声ソースとしてSoundPlayerに渡すことによってボリュームを変更する。

実装例

以下のコードでは、WAVE形式のデータを読み込むクラスWaveStreamを作成し、それをSoundPlayerのコンストラクタに渡している。 WaveStreamのReadメソッドを呼び出すと、読み込んだWAVEデータのヘッダ部分はそのまま返すが、dataチャンク内の各音声サンプルについては音量を変更した上で返すように実装している。 元の音声より大きい音量にすることはできない。

なお、以下のコードにおけるWaveStreamクラスは16ビットのWAVE形式の音声のみに対応している。 モノラル・ステレオなど、チャンネル数は任意のものに対応している。 また、RIFFヘッダのチェック等は省略しているので、ヘッダの内容によっては予期しない動作となる可能性もある。

using System;
using System.IO;
using System.Media;

class Sample {
  static void Main()
  {
    const string waveFile = "test.wav";

    using (var stream = new WaveStream(File.OpenRead(waveFile))) {
      // 0 から 100の範囲で音量を指定する
      stream.Volume = 75;

      // WaveStreamから音声を読み込みSoundPlayer.PlaySyncメソッドで再生
      using (var player = new SoundPlayer(stream)) {
        player.PlaySync();
      }
    }
  }
}

class WaveStream : Stream {
  public override bool CanSeek {
    // シークはサポートしない
    get { return false; }
  }

  public override bool CanRead {
    get { return !IsClosed; }
  }

  public override bool CanWrite {
    // 書き込みはサポートしない
    get { return false; }
  }

  private bool IsClosed {
    get { return reader == null; }
  }

  public override long Position {
    get { CheckDisposed(); throw new NotSupportedException(); }
    set { CheckDisposed(); throw new NotSupportedException(); }
  }

  public override long Length {
    get { CheckDisposed(); throw new NotSupportedException(); }
  }

  public int Volume {
    get { CheckDisposed(); return volume; }
    set {
      CheckDisposed();

      if (value < 0 || MaxVolume < value)
        throw new ArgumentOutOfRangeException("Volume",
                                              value,
                                              string.Format("0から{0}の範囲の値を指定してください", MaxVolume));

      volume = value;
    }
  }

  public WaveStream(Stream baseStream)
  {
    if (baseStream == null)
      throw new ArgumentNullException("baseStream");
    if (!baseStream.CanRead)
      throw new ArgumentException("読み込み可能なストリームを指定してください", "baseStream");

    this.reader = new BinaryReader(baseStream);

    ReadHeader();
  }

  public override void Close()
  {
    if (reader != null) {
      reader.Close();
      reader = null;
    }
  }

  // dataチャンクまでのヘッダブロックの内容をバッファに読み込んでおく
  // WAVEFORMAT等のヘッダ内容のチェックは省略
  private void ReadHeader()
  {
    using (var headerStream = new MemoryStream()) {
      var writer = new BinaryWriter(headerStream);

      // RIFFヘッダ
      var riffHeader = reader.ReadBytes(12);

      writer.Write(riffHeader);

      // dataチャンクまでの内容をwriterに書き写す
      for (;;) {
        var chunkHeader = reader.ReadBytes(8);

        writer.Write(chunkHeader);

        var fourcc = BitConverter.ToInt32(chunkHeader, 0);
        var size = BitConverter.ToInt32(chunkHeader, 4);

        if (fourcc == 0x61746164) // 'data'
          break;

        writer.Write(reader.ReadBytes(size));
      }

      writer.Close();

      header = headerStream.ToArray();
    }
  }

  public override int Read(byte[] buffer, int offset, int count)
  {
    CheckDisposed();

    if (buffer == null)
      throw new ArgumentNullException("buffer");
    if (offset < 0)
      throw new ArgumentOutOfRangeException("offset", offset, "0以上の値を指定してください");
    if (count < 0)
      throw new ArgumentOutOfRangeException("count", count, "0以上の値を指定してください");
    if (buffer.Length - count < offset)
      throw new ArgumentException("配列の範囲を超えてアクセスしようとしました", "offset");

    if (header == null) {
      // dataチャンクの読み込み
      // WAVEサンプルを読み込み、音量を適用して返す
      // ストリームは16ビット(1サンプル2バイト)と仮定

      // countバイト以下となるよう読み込むサンプル数を決定する
      var samplesToRead = count / 2;
      var bytesToRead = samplesToRead * 2;
      var len = reader.Read(buffer, offset, bytesToRead);

      if (len == 0)
        return 0; // 終端まで読み込んだ

      // 読み込んだサンプル1つずつにボリュームを適用する
      for (var sample = 0; sample < samplesToRead; sample++) {
        short s = (short)(buffer[offset] | (buffer[offset + 1] << 8));

        s = (short)(((int)s * volume) / MaxVolume);

        buffer[offset] = (byte)(s & 0xff);
        buffer[offset + 1] = (byte)((s >> 8) & 0xff);

        offset += 2;
      }

      return len;
    }
    else {
      // ヘッダブロックの読み込み
      // バッファに読み込んでおいた内容をそのままコピーする
      var bytesToRead = Math.Min(header.Length - headerOffset, count);

      Buffer.BlockCopy(header, headerOffset, buffer, offset, bytesToRead);

      headerOffset += bytesToRead;

      if (headerOffset == header.Length)
        // ヘッダブロックを全て読み込んだ
        // (不要になったヘッダのバッファを解放し、以降はdataチャンクの読み込みに移る)
        header = null;

      return bytesToRead;
    }
  }

  public override void SetLength(long @value)
  {
    CheckDisposed();

    throw new NotSupportedException();
  }

  public override long Seek(long offset, SeekOrigin origin)
  {
    CheckDisposed();

    throw new NotSupportedException();
  }

  public override void Flush()
  {
    CheckDisposed();

    throw new NotSupportedException();
  }

  public override void Write(byte[] buffer, int offset, int count)
  {
    CheckDisposed();

    throw new NotSupportedException();
  }

  private void CheckDisposed()
  {
    if (IsClosed)
      throw new ObjectDisposedException(GetType().FullName);
  }

  private BinaryReader reader;
  private byte[] header;
  private int headerOffset = 0;
  private int volume = MaxVolume;
  private const int MaxVolume = 100;
}
Imports System
Imports System.IO
Imports System.Media

Class Sample
  Shared Sub Main()
    Const waveFile As String = "test.wav"

    Using stream As New WaveStream(File.OpenRead(waveFile))
      ' 0 から 100の範囲で音量を指定
      stream.Volume = 75

      ' WaveStreamから音声を読み込みSoundPlayer.PlaySyncメソッドで再生
      Using player As New SoundPlayer(stream)
        player.PlaySync()
      End Using
    End Using
  End Sub
End Class

Class WaveStream
  Inherits Stream

  Public Overrides ReadOnly Property CanSeek As Boolean
    Get
      ' シークはサポートしない
      Return False
    End Get
  End Property

  Public Overrides ReadOnly Property CanRead As Boolean
    Get
      Return Not IsClosed
    End Get
  End Property

  Public Overrides ReadOnly Property CanWrite As Boolean
    Get
      ' 書き込みはサポートしない
      Return False
    End Get
  End Property

  Private ReadOnly Property IsClosed As Boolean
    Get
      Return reader Is Nothing
    End Get
  End Property

  Public Overrides Property Position As Long
    Get
      CheckDisposed()
      Throw New NotSupportedException()
    End Get
    Set(ByVal value As Long)
      CheckDisposed()
      Throw New NotSupportedException()
    End Set
  End Property

  Public Overrides ReadOnly Property Length As Long
    Get
      CheckDisposed()
      Throw New NotSupportedException()
    End Get
  End Property

  Public Property Volume As Integer
    Get
      CheckDisposed()
      Return _volume
    End Get
    Set(ByVal value As Integer)
      CheckDisposed()

      If value < 0 OrElse MaxVolume < value Then
        Throw New ArgumentOutOfRangeException("Volume", _
                                              value, _
                                              String.Format("0から{0}の範囲の値を指定してください", MaxVolume))
      End If

      _volume = value
    End Set
  End Property

  Public Sub New(ByVal baseStream As Stream)
    If baseStream Is Nothing Then Throw New ArgumentNullException("baseStream")
    If Not baseStream.CanRead Then Throw New ArgumentException("読み込み可能なストリームを指定してください", "baseStream")

    reader = New BinaryReader(baseStream)

    ReadHeader()
  End Sub

  Public Overrides Sub Close()
    If Not reader Is Nothing Then
      reader.Close()
      reader = Nothing
    End If
  End Sub

  ' dataチャンクまでのヘッダブロックの内容をバッファに読み込んでおく
  ' WAVEFORMAT等のヘッダ内容のチェックは省略
  Private Sub ReadHeader()
    Using headerStream As New MemoryStream()
      Dim writer As New BinaryWriter(headerStream)

      ' RIFFヘッダ
      Dim riffHeader() As Byte = reader.ReadBytes(12)

      writer.Write(riffHeader)

      ' dataチャンクまでの内容をwriterに書き写す
      Do
        Dim chunkHeader() As Byte = reader.ReadBytes(8)

        writer.Write(chunkHeader)

        Dim fourcc As Integer = BitConverter.ToInt32(chunkHeader, 0)
        Dim size As Integer = BitConverter.ToInt32(chunkHeader, 4)

        If fourcc = &h61746164 Then Exit Do 'data'

        writer.Write(reader.ReadBytes(size))
      Loop

      writer.Close()

      header = headerStream.ToArray()
    End Using
  End Sub

  Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer
    CheckDisposed()

    If buffer Is Nothing Then Throw New ArgumentNullException("buffer")
    If offset < 0 Then Throw New ArgumentOutOfRangeException("offset", offset, "0以上の値を指定してください")
    IF count < 0 Then Throw New ArgumentOutOfRangeException("count", count, "0以上の値を指定してください")
    If buffer.Length - count < offset Then Throw New ArgumentException("配列の範囲を超えてアクセスしようとしました", "offset")

    If header Is Nothing Then
      ' dataチャンクの読み込み
      ' WAVEサンプルを読み込み、音量を適用して返す
      ' ストリームは16ビット(1サンプル2バイト)と仮定

      ' countバイト以下となるよう読み込むサンプル数を決定する
      Dim samplesToRead As Integer = count \ 2
      Dim bytesToRead As Integer = samplesToRead * 2
      Dim len As Integer = reader.Read(buffer, offset, bytesToRead)

      If len = 0 Then Return 0 ' 終端まで読み込んだ

      ' 読み込んだサンプル1つずつにボリュームを適用する
      For sample As Integer = 0 To samplesToRead - 1
        Dim s As Short = CShort(buffer(offset)) Or (CShort(buffer(offset + 1)) << 8)

        s = CShort((CInt(s) * _volume) \ MaxVolume)

        buffer(offset) = CByte(s And &hFF)
        buffer(offset + 1) = CByte((s >> 8) And &hFF)

        offset += 2
      Next

      Return len
    Else
      ' ヘッダブロックの読み込み
      ' バッファに読み込んでおいた内容をそのままコピーする
      Dim bytesToRead As Integer = Math.Min(header.Length - headerOffset, count)

      System.Buffer.BlockCopy(header, headerOffset, buffer, offset, bytesToRead)

      headerOffset += bytesToRead

      If headerOffset = header.Length Then
        ' ヘッダブロックを全て読み込んだ
        ' (不要になったヘッダのバッファを解放し、以降はdataチャンクの読み込みに移る)
        header = Nothing
      End If

      Return bytesToRead
    End If
  End Function

  Public Overrides Sub SetLength(ByVal value As Long)
    CheckDisposed()

    Throw New NotSupportedException()
  End Sub

  Public Overrides Function Seek(ByVal offset As Long, ByVal origin As SeekOrigin) As Long
    CheckDisposed()

    Throw New NotSupportedException()
  End Function

  Public Overrides Sub Flush()
    CheckDisposed()

    Throw New NotSupportedException()
  End Sub

  Public Overrides Sub Write(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer)
    CheckDisposed()

    Throw New NotSupportedException()
  End Sub

  Private Sub CheckDisposed()
    If IsClosed Then Throw New ObjectDisposedException(Me.GetType().FullName)
  End Sub

  Private reader As BinaryReader
  Private header() As Byte
  Private headerOffset As Integer = 0
  Private _volume As Integer = MaxVolume
  Private Const MaxVolume As Integer = 100
End Class

その他の変更方法