doclist 阅读(40) 评论(0)

特定于平台的声音生成

现在为了本章的真正目的:给MonkeyTap发声。所有三个平台都支持API,允许程序动态生成和播放音频波形。这是MonkeyTapWithSound程序采用的方法。
商业音乐文件通常以诸如MP3之类的格式压缩。但是当一个程序算法算法生成波形时,未压缩的格式会更加方便。最基本的技术 - 所有三个平台都支持 - 称为脉冲编码调制或PCM。除了花哨的名字,它很简单,它是用于在音乐CD上存储声音的技术。
PCM波形由一系列恒定速率的样本描述,称为采样率。音乐CD使用标准速率为每秒44,100个样本。如果不需要高音质,计算机程序生成的音频文件通常使用一半(22,050)或四分之一(11,025)的采样率。可记录和再现的最高频率是采样率的一半。
每个样本都是固定大小,用于定义该时间点波形的幅度。音乐CD上的样本是带符号的16位值。当声音质量无关紧要时,8位样本很常见。某些环境支持浮点值。多个样本可以容纳立体声或任意数量的声道。对于移动设备上的简单音效,单声道声音通常很好。
MonkeyTapWithSound中的声音生成算法是针对16位单声道样本进行硬编码的,但采样率由常量指定,并且可以轻松更改。
现在您已经了解了DependencyService的工作原理,让我们检查添加到MonkeyTap的代码,将其转换为MonkeyTapWithSound,让我们从上到下看一下。为避免重现大量代码,新项目包含MonkeyTap项目中MonkeyTap.xaml和MonkeyTap.xaml.cs文件的链接。
在Visual Studio中,通过从项目菜单中选择“添加”>“现有项”,可以将项目添加为项目作为现有文件的链接。然后使用“添加现有项”对话框导航到该文件。从“添加”按钮的下拉列表中选择“添加为链接”。
在Xamarin Studio中,从项目的工具菜单中选择添加>添加文件。打开文件后,会弹出“添加文件到文件夹”警告框。选择“添加指向该文件的链接”。
但是,在Visual Studio中执行这些步骤后,还需要手动编辑Mon?keyTapWithSound.csproj文件,以将MonkeyTapPage.xaml文件更改为EmbeddedResource,将Generator更改为MSBuild:UpdateDesignTimeXaml。此外,还将一个DependentUpon标记添加到MonkeyTapPage.xaml.cs文件中以引用MonkeyTapPage.xaml文件。这会导致代码隐藏文件在文件列表中的XAML文件下缩进。
然后,MonkeyTapWithSoundPage类派生自MonkeyTapPage类。虽然MonkeyTapPage类是由XAML文件和代码隐藏文件定义的,但MonkeyTapWithSoundPage仅是代码。当以这种方式派生类时,必须将XAML文件中的事件的原始代码隐藏文件中的事件处理程序定义为受保护的,这是这种情况。
MonkeyTap类还将flashDuration常量定义为protected,并将两个方法定义为protected和virtual。 MonkeyTapWithSoundPage重写这两个方法来调用一个名为SoundPlayer.PlaySound的静态方法:

namespace MonkeyTapWithSound
{
    class MonkeyTapWithSoundPage : MonkeyTap.MonkeyTapPage
    {
        const int errorDuration = 500;
        // Diminished 7th in 1st inversion: C, Eb, F#, A
        double[] frequencies = { 523.25, 622.25, 739.99, 880 };
        protected override void BlinkBoxView(int index)
        {
            SoundPlayer.PlaySound(frequencies[index], flashDuration);
            base.BlinkBoxView(index);
        }
        protected override void EndGame()
        {
            SoundPlayer.PlaySound(65.4, errorDuration);
            base.EndGame();
        }
    }
}

SoundPlayer.PlaySound方法接受频率和持续时间(以毫秒为单位)。 每一件事 - 音量,声音的谐波组成以及声音是如何产生的 - 都是PlaySound方法的责任。 但是,此代码隐含地假设SoundPlayer.PlaySound立即返回,并且不等待声音完成播放。 幸运的是,所有这三个平台都支持以这种方式运行的声音生成API。
使用PlaySound静态方法的SoundPlayer类是MonkeyTapWithSound PCL项目的一部分。 此方法的职责是为声音定义PCM数据的数组。 此数组的大小取决于采样率和持续时间。 for循环计算定义所请求频率的三角波的样本:

namespace MonkeyTapWithSound
{
    class SoundPlayer
    {
        const int samplingRate = 22050;
        /* Hard-coded for monaural, 16-bit-per-sample PCM */
        public static void PlaySound( double frequency = 440, int duration = 250 )
        {
            short[] shortBuffer    = new short[samplingRate * duration / 1000];
            double    angleIncrement    = frequency / samplingRate;
            double    angle        = 0; /* normalized 0 to 1 */
            for ( int i = 0; i < shortBuffer.Length; i++ )
            {
                /* Define triangle wave */
                double sample;
                /* 0 to 1 */
                if ( angle < 0.25 )
                    sample = 4 * angle;
                /* 1 to -1 */
                else if ( angle < 0.75 )
                    sample = 4 * (0.5 - angle);
                /* -1 to 0 */
                else
                    sample = 4 * (angle - 1);
                shortBuffer[i]    = (short) (32767 * sample);
                angle        += angleIncrement;
                while ( angle > 1 )
                    angle -= 1;
            }
            byte[] byteBuffer = new byte[2 * shortBuffer.Length];
            Buffer.BlockCopy( shortBuffer, 0, byteBuffer, 0, byteBuffer.Length );
            DependencyService.Get().PlaySound( samplingRate, byteBuffer );
        }
    }
} 

虽然样本是16位整数,但是其中两个平台希望数据以字节数组的形式存在,因此使用Buffer.BlockCopy在末尾附近进行转换。 该方法的最后一行使用DependencyService将具有采样率的此字节数组传递给各个平台。
DependencyService.Get方法引用IPlatformSoundPlayer接口,该接口定义了PlaySound方法的签名:

namespace MonkeyTapWithSound
{
    public interface IPlatformSoundPlayer
    {
        void PlaySound(int samplingRate, byte[] pcmData);
    }
}

现在来了困难的部分:为三个平台编写这个PlaySound方法!
iOS版本使用AVAudioPlayer,它需要包含Wave?form音频文件格式(.wav)文件中使用的标头的数据。 这里的代码汇编了MemoryBuffer中的数据,然后将其转换为NSData对象:

using System;
using System.IO;
using System.Text;
using Xamarin.Forms;
using AVFoundation;
using Foundation;
[assembly: Dependency( typeof(MonkeyTapWithSound.iOS.PlatformSoundPlayer) )]
namespace MonkeyTapWithSound.iOS
{
    public class PlatformSoundPlayer : IPlatformSoundPlayer
    {
        const int    numChannels    = 1;
        const int    bitsPerSample    = 16;
        public void PlaySound( int samplingRate, byte[] pcmData )
        {
            int        numSamples    = pcmData.Length / (bitsPerSample / 8);
            MemoryStream    memoryStream    = new MemoryStream();
            BinaryWriter    writer        = new BinaryWriter( memoryStream, Encoding.ASCII );
            /* Construct WAVE header. */
            writer.Write( new char[] { 'R', 'I', 'F', 'F' } );
            writer.Write( 36 + sizeof(short) * numSamples );
            writer.Write( new char[] { 'W', 'A', 'V', 'E' } );
            writer.Write( new char[] { 'f', 'm', 't', ' ' } );              /* format chunk */
            writer.Write( 16 );                                             /* PCM chunk size */
            writer.Write( (short) 1 );                                      /* PCM format flag */
            writer.Write( (short) numChannels );
            writer.Write( samplingRate );
            writer.Write( samplingRate * numChannels * bitsPerSample / 8 ); /* byte rate */
            writer.Write( (short) (numChannels * bitsPerSample / 8) );      /* block align */
            writer.Write( (short) bitsPerSample );
            writer.Write( new char[] { 'd', 'a', 't', 'a' } );              /* data chunk */
            writer.Write( numSamples * numChannels * bitsPerSample / 8 );
            /* Write data as well. */
            writer.Write( pcmData, 0, pcmData.Length );
            memoryStream.Seek( 0, SeekOrigin.Begin );
            NSData        data        = NSData.FromStream( memoryStream );
            AVAudioPlayer    audioPlayer    = AVAudioPlayer.FromData( data );
            audioPlayer.Play();
        }
    }
}

请注意两个要点:PlatformSoundPlayer实现IPlatformSoundPlayer接口,并使用Dependency属性标记类。
Android版本使用AudioTrack类,结果更容易一些。 但是,AudioTrack对象不能重叠,所以有必要保存前一个对象并停止播放,然后开始下一个对象:

using System;
using Android.Media;
using Xamarin.Forms;
[assembly: Dependency( typeof(MonkeyTapWithSound.Droid.PlatformSoundPlayer) )]
namespace MonkeyTapWithSound.Droid
{
    public class PlatformSoundPlayer : IPlatformSoundPlayer
    {
        AudioTrack previousAudioTrack;
        public void PlaySound( int samplingRate, byte[] pcmData )
        {
            if ( previousAudioTrack != null )
            {
                previousAudioTrack.Stop();
                previousAudioTrack.Release();
            }
            AudioTrack audioTrack = new AudioTrack( Stream.Music,
                                samplingRate,
                                ChannelOut.Mono,
                                Android.Media.Encoding.Pcm16bit,
                                pcmData.Length * sizeof(short),
                                AudioTrackMode.Static );
            audioTrack.Write( pcmData, 0, pcmData.Length );
            audioTrack.Play();
            previousAudioTrack = audioTrack;
        }
    }
}

三个Windows和Windows Phone平台可以使用MediaStreamSource。 为了避免大量重复代码,MonkeyTapWithSound解决方案包含一个名为WinRuntimeShared的额外SAP项目,该项目仅由三个平台都可以使用的类组成:

using System;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Media.Core;
using Windows.Media.MediaProperties;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Controls;
namespace MonkeyTapWithSound.WinRuntimeShared
{
    public class SharedSoundPlayer
    {
        MediaElement    mediaElement = new MediaElement();
        TimeSpan    duration;
        public void PlaySound( int samplingRate, byte[] pcmData )
        {
            AudioEncodingProperties audioProps =
                AudioEncodingProperties.CreatePcm( (uint) samplingRate, 1, 16 );
            AudioStreamDescriptor    audioDesc    = new AudioStreamDescriptor( audioProps );
            MediaStreamSource    mss        = new MediaStreamSource( audioDesc );
            bool            samplePlayed    = false;
            mss.SampleRequested += (sender, args) =>
            {
                if ( samplePlayed )
                    return;
                IBuffer            ibuffer = pcmData.AsBuffer();
                MediaStreamSample    sample    =
                    MediaStreamSample.CreateFromBuffer( ibuffer, TimeSpan.Zero );
                sample.Duration        = TimeSpan.FromSeconds( pcmData.Length / 2.0 / samplingRate );
                args.Request.Sample    = sample;
                samplePlayed        = true;
            };
            mediaElement.SetMediaStreamSource( mss );
        }
    }
}

此SAP项目由三个Windows和Windows Phone项目引用,每个项目包含相同的(命名空间除外)PlatformSoundPlayer类:

using System;
using Xamarin.Forms;
[assembly: Dependency( typeof(MonkeyTapWithSound.UWP.PlatformSoundPlayer) )]
namespace MonkeyTapWithSound.UWP
{
    public class PlatformSoundPlayer : IPlatformSoundPlayer
    {
        WinRuntimeShared.SharedSoundPlayer sharedSoundPlayer;
        public void PlaySound( int samplingRate, byte[] pcmData )
        {
            if ( sharedSoundPlayer == null )
            {
                sharedSoundPlayer = new WinRuntimeShared.SharedSoundPlayer();
            }
            sharedSoundPlayer.PlaySound( samplingRate, pcmData );
        }
    }
}

使用DependencyService来执行特定于平台的杂务是非常强大的,但是当涉及到用户界面元素时,这种方法不足。 如果您需要扩展装饰Xamarin.Forms应用程序页面的视图库,那么这项工作涉及创建特定于平台的渲染器,这是本书最后一章中讨论的过程。