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