전체 페이지뷰

2017년 4월 25일 화요일

Chapter 9. Platform-specific API calls

비상사태가 발생했습니다. 이전 장에서 MonkeyTap을 해본 사람이라면 누구나 같은 결론에 도달하게 될 지극히 기본적인 것입니다.
MonkeyTap에는 사운드가 필요합니다.

대단한 소리가 필요한 것도 아닙니다. 그저 BoxView가 깜빡일 때 삑 소리만 나도 됩니다. 그러나 Xamarin.Forms API는 사운드를 지원하지 않으므로 API 호출 몇번해서 해결될 문제가 아닙니다. 사운드를 지원하기 위해서는 플랫폼별 사운드 생성 기능을 사용해야 하므로 Xamarin.Forms 이상의 것이 필요합니다. iOS, Android, Windows Phone에서 사운드를 만드는 방법을 알아내는 것도 어려운 일이지만, 자마린이 개별 플랫폼을 호출하려면 어떻게 해야 하는 건지도 알아야 하겠습니다.

사운드를 만드는 복잡한 과제에 도전하기 전에, 훨씬 간단한 예제를 통해 플랫폼별 API 호출법들을 살펴 보겠습니다. 이번 챕터에 나오는 처음 세 개의 프로그램은 모두 기능상 동일합니다. 그것들은 모두 각 플랫폼의 운영체제에 의해 제공되는 두 가지 정보(실행되는 장치의 모델과 운영 체제 버전)를 보여줍니다.


Preprocessing in the Shared Asset Project


2장에서 여러분은 SAP(Shared Asset Project) 또는 PCL(Portable Class Library)을 사용할 수 있다는 것을 배우셨습니다. SAP에는 플랫폼 프로젝트간에 공유되는 코드 파일이 들어 있는 반면, PCL은 public type을 통해서만 접근할 수 있는 공통 코드 라이브러리가 들어있습니다.

Shared Asset Project에서 플랫폼 API에 액세스하는 것은 전통적인 프로그래밍 툴을 이용하므로 PCL에서보다 간단합니다. 먼저 이 방법을 시도해 봅시다. SAP으로 자마린 솔루션을 생성하고 PCL에서 했던 것처엄 XAML 기반 ContentPage를 추가해 봅시다.

아래는 플랫폼 정보를 출력하는 PlatInfoSap1이라는 XAML 파일입니다.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:PlatInfoSap1"
             x:Class="PlatInfoSap1.MainPage">
    <StackLayout Padding="20">
        <StackLayout VerticalOptions="CenterAndExpand">
            <Label Text="Device Model:" />
            <ContentView Padding="50,0,0,0">
                <Label x:Name="modelLabel"
                       FontSize="Large"
                       FontAttributes="Bold" />
            </ContentView>
        </StackLayout>
        <StackLayout VerticalOptions="CenterAndExpand">
            <Label Text="Operating system version:" />
            <ContentView Padding="50,0,0,0">
                <Label x:Name="versionLabel"
                       FontSize="Large"
                       FontAttributes="Bold" />
            </ContentView>
        </StackLayout>
    </StackLayout>
</ContentPage>
cs

code-behind 파일은 modelLabelversionLabelText 프로퍼티를 설정해야 합니다.

SAP의 코드 파일은 개별 플랫폼의 코드를 확장한 것입니다. 즉, SAP의 코드는 2 장과 4 장에서 설명 된 것처럼  # 전 처리기 지시문 #if, #elif, #else, #endif를 세 플랫폼 용으로 정의 된 조건부 컴파일 기호와 함께 사용할 수 있습니다. 그 조건부 컴파일 기호들은 아래와 같습니다.(2장에서 배운 것처럼 각 프로젝트명을 우클릭하고 속성을 선택하여 나오는 창에서 빌드를 선택하고 전처리 지시어를 지정하는 것을 빼먹지 마시기 바랍니다. 전 오랜만에 SAP를 쓰다보니 한참 헤맸습니다.)


  • __IOS__ : iOS
  • __ANDROID__ : Android
  • WINDOWS_UWP : the Universal Windows Platform
  • WINDOWS_APP : Windows 8.1
  • WINDOWS_PHONE_APP : Windows Phone 8.1

물론 모델 및 버전 정보를 얻는데 관련된 API는 다음 세 플랫폼에서 모두 다릅니다.


  • iOS는 UIKit 네임스페이스의 UIDevice 클래스
  • Android는 Android.OS 네임스페이스의  Build 클래스 프로퍼티 
  • Windows 플랫폼은 Windows.Security.ExchangeActiveSyncProvisioning 네임스페이스의 EasClientDeviceInformation 클래스


다음은 조건부 컴파일 기호를 기반으로 modelLabel, versionLabel이 설정되는 방법을 보여주는 PlatInfoSap1.xaml.cs code-behind 파일입니다.

using System;
using Xamarin.Forms;
#if __IOS__
using UIKit;
#elif __ANDROID__
using Android.OS;
#elif WINDOWS_APP || WINDOWS_PHONE_APP || WINDOWS_UWP
using Windows.Security.ExchangeActiveSyncProvisioning;
#endif
namespace PlatInfoSap1
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
#if __IOS__
            UIDevice device=new UIDevice();
            modelLabel.Text=device.Model.ToString();
            versionLabel.Text=String.Format("{0} {1}", device.SystemName, 
                                                       device.SystemVersion);
#elif __ANDROID__
            modelLabel.Text = String.Format("{0} {1}", Build.Manufacturer, 
                                                       Build.Model);
            versionLabel.Text = Build.VERSION.Release.ToString();
#elif WINDOWS_APP || WINDOWS_PHONE_APP || WINDOWS_UWP
            EasClientDeviceInformation devInfo=new EasClientDeviceInformation();
            modelLabel.Text=string.Format("{0} {1}",devInfo.SystemManufacturer, 
                                                    devInfo.SystemProductName);
            versionLabel.Text=devInfo.OperatingSystem;
#endif
        }
    }
}
cs

이러한 전처리 지시문은 다양한 지시어를 선택하고 플랫폼 별 API를 호출하는 데 사용됩니다. 이처럼 간단한 프로그램에서는 클래스 이름과 함께 네임스페이스를 다 사용할 수도 있지만 더 긴 코드 블록의 경우 using 지시어를 사용하는 것이 좋습니다.

물론 프로그램은 잘 작동합니다.



이 방식의 장점은 세개의 플랫폼에 대한 코드를 한 곳에 가지고 있다는 것입니다. 하지만 코드에 들어간 전처리 지시문은 볼썽 사나워서 프로그래밍 초기 시대로 돌아가는 듯한 모습입니다. 이 예제와 같이 짧고 빈번하지 않은 호출에 전처리 지시문을 사용하는 것은 바람직하지 않을 수도 있지만, 더 큰 프로그램에서는 플랫폼별 코드 및 공유코드 블록을 잘 조율해야 하며 너무 많은 전처리 지시문이 나오면 혼란스러울 수 있습니다. 따라서 전처리 지시문은 거의 수정하지 않고 사용될수 있어야 하며, 일반적으로 앱의 구조적 요소로 사용해서는 안 됩니다.

이제 다른 접근 방식을 살펴봅시다.


Parallel classes and the Shared Asset Project

SAP가 플랫폼 프로젝트의 extension이긴 하지만, 이 관계는 쌍방향입니다. 플랫폼 프로젝트가 SAP에서 코드를 호출할 수 있는 것처럼, SAP도 개별 플랫폼 프로젝트를 호출할 수 있습니다.

이것은 곧 플랫폼별 API 호출을 개별 플랫폼 프로젝트의 클래스로 제한할 수 있다는 뜻입니다. 플랫폼 프로젝트 내에서 클래스의 이,름과 네임스페이스가 동일하면 SAP의 코드는 플랫폼 독립적인 방식으로 이러한 클래스에 액세스할 수 있습니다.

PlatInfoSap2 솔루션에서 5개의 플랫폼 프로젝트 각각에는 GetModelGetVersion이라는 문자열 개체를 반환하는 두 개의 메서드가 들어있는 PlatformInfo라는 클래스가 있습니다. 아래는 그 중 iOS 버전입니다.

using System;
using UIKit;
namespace PlatInfoSap2
{
    public class PlatformInfo
    {
        UIDevice device = new UIDevice();
        public string GetModel()
        {
            return device.Model.ToString();
        }
        public string GetVersion()
        {
            return String.Format("{0} {1}", device.SystemName, 
                device.SystemVersion);
        }
    }
}
cs

네임스페이스를 확인 하시기 바랍니다. 이 iOS 프로젝트의 다른 클래스는 PlatInfoSap2.iOS 네임스페이스를 사용하지만 이 클래스의 네임스페이스는 PlatInfoSap2입니다. 이렇게 해서 SAP는 특정 플랫폼 없이도 이 클래스를 직접 액세스 할 수 있습니다.

다음은 Android 프로젝트의 평행 클래스입니다. 동일한 네임스페이스, 클래스이름, 메소드 이름이지만 Android API 호출을 사용하는 메소드의 구현만 다릅니다.

using System;
using Android.OS;
namespace PlatInfoSap2
{
    public class PlatformInfo
    {
        public string GetModel()
        {
            return String.Format("{0} {1}", Build.Manufacturer, 
                Build.Model);
        }
        public string GetVersion()
        {
            return Build.VERSION.Release.ToString();
        }
    }
}
cs

아래는 Windows 버전입니다.
using System;
using Windows.Security.ExchangeActiveSyncProvisioning;
namespace PlatInfoSap2
{
    public class PlatformInfo
    {
        EasClientDeviceInformation devInfo = new EasClientDeviceInformation();
        public string GetModel()
        {
            return String.Format("{0} {1}", devInfo.SystemManufacturer, 
                devInfo.SystemProductName);
        }
        public string GetVersion()
        {
            return devInfo.OperatingSystem;
        }
    }
}
cs


PlatInfoSap2 프로젝트의 XAML 파일은 기본적으로 PlatInfoSap1 프로젝트의 XAML 파일과 동일합니다. code-behind 파일은 상당히 간단합니다.

using System;
using Xamarin.Forms;
namespace PlatInfoSap2
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            PlatformInfo platformInfo = new PlatformInfo();
            modelLabel.Text = platformInfo.GetModel();
            versionLabel.Text = platformInfo.GetVersion();
        }
    }
}
cs

클래스에서 참조하는 PlatformInfo의 버전은 컴파일된 프로젝트에 속한 것입니다. 마치 개별 플랫폼 프로젝트에 있는 Xamarin.Forms에 약간의 확장을 정의한 것과 같습니다.


DependencyService and the Portable Class Library


PlatInfoSap2 프로그램에 설명된 기술을 PCL 솔루션에 구현할 수 있을까요? 얼핏 불가능해 보일지도 모릅니다. 어플리케이션 프로젝트는 항상 라이브러리를 호출하지만, 일반적으로 라이브러리는 이벤트나 콜백 함수의 컨텍스트를 제외하고 어플리케이션을 호출할 수 없습니다. PCL은 장치 독립적인 .NET 버전과 함께 번들로 제공되며 자체 또는 다른 참조 가능한 PCL 내에서 코드를 실행할 수 있는 기능만 제공합니다.

그러나, Xamarin.Forms 어플리케이션이 실행 중일 때 .NET Reflection을 사용하여 자체 또는 다른 프로그램의 어셈블리에 액세스할 수 있습니다. 다시 말해 PCL의 코드가 reflection을 사용하여 PCL이 참조되는 플랫폼 어셈블리에 있는 클래스에 액세스할 수 있다는 것입니다. 물론 그 클래스는 public 선언되어야 하지만 요구사항은 그것 뿐입니다.

이 기술을 활용하는 코드를 작성하기 전에 이 솔루션이 DependencyService라는 Xamarin.Forms 클래스의 형태로  이미 존재한다는 것을 알아야합니다. 이 클래스는 .NET 리플렉션을 사용하여 응용 프로그램의 다른 모든 어셈블리 (특정 플랫폼 어셈블리 포함)를 검색하고 플랫폼별 코드에 대한 액세스를 제공합니다.

DependencyService 사용법은 DisplayPlatformInfo 솔루션에 설명되어 있으며,이 솔루션은 공유 코드로 PCL을 사용합니다. 플랫폼 프로젝트에서 구현하려는 메소드를 선언하는 PCL 프로젝트 인터페이스를 정의하는 것으로 DependencyService를 사용하는 과정을 시작합니다. IPlatformInfo가 그것입니다.

namespace DisplayPlatformInfo
{
    public interface IPlatformInfo
    {
        string GetModel();
        string GetVersion();
    }
}
cs

이 두개의 메소드는 지금껏 계속 PlatformInfo 클래스 등에서 계속 보아온 것입니다.

PlatInfoSap2와 유사한 방법으로 DisplayPlatformInfo의 세 플랫폼별 프로젝트는 이제 IPlatformInfo를 구현하는 클래스를 가져야 합니다. 다음은 iOS 프로젝트에 구현된  PlatformInfo라는 클래스입니다.

using System;
using UIKit;
using Xamarin.Forms;
[assembly:Dependency(typeof(DisplayPlatformInfo.iOS.PlatformInfo))]
namespace DisplayPlatformInfo.iOS
{
    public class PlatformInfo : IPlatformInfo
    {
        UIDevice device = new UIDevice();
        public string GetModel()
        {
            return device.Model.ToString();
        }
        public string GetVersion()
        {
            return String.Format("{0} {1}", device.SystemName, 
                device.SystemVersion);
        }
    }
}
cs

이 클래스는 PCL에서 직접 참조되지 않으므로, 네임스페이스는 원하는 대로 사용할 수 있습니다. 여기서는 iOS 프로젝트의 다른 네임스페이스와 동일하게 설정했습니다. 클래스 이름도 마음대로 해도 상관없습니다. 그러나 이름을 무엇으로 하던간에 PCL에 정의된 IPlatformInfo 인터페이스를 구현해야 합니다:

public class PlatformInfo : IPlatformInfo
cs

그리고 이 클래스는 네임스페이스 블록 외부의 attribute에서 참조되어야 합니다. using문 아래에서 그것을 볼 수 있습니다.

[assembly: Dependency(typeof(DisplayPlatformInfo.iOS.PlatformInfo))]
cs

Dependency attribute를 정의하는 DependencyAttribute 클래스는 Xamarin.Forms의 일부이며 주로 DependencyService와 함께 사용됩니다. 인수는 PCL이 액세스할 수 있는 플랫폼 프로젝트의 클래스 Type 객체이며, 이 경우는 PlatformInfo 클래스입니다. 이 attribute는 플랫폼 어셈블리 자체에 연결되므로 PCL에서 실행되는 코드가 라이브러리 전체를 검색할 필요가 없습니다.

다음은 PlatformInfo의 안드로이드 버전입니다.

using System;
using Android.OS;
using Xamarin.Forms;
[assembly:Dependency(typeof(DisplayPlatformInfo.Droid.PlatformInfo))]
namespace DisplayPlatformInfo.Droid
{
    public class PlatformInfo : IPlatformInfo
    {
        public string GetModel()
        {
            return String.Format("{0} {1}", Build.Manufacturer,
                                            Build.Model);
        }
        public string GetVersion()
        {
            return Build.VERSION.Release.ToString();
        }
    }
}
cs

그리고, 다음은 UWP 버전입니다.

using System;
using Windows.Security.ExchangeActiveSyncProvisioning;
using Xamarin.Forms;
[assembly: Dependency(typeof(DisplayPlatformInfo.UWP.PlatformInfo))]
namespace DisplayPlatformInfo.UWP
{
    public class PlatformInfo : IPlatformInfo
    {
        EasClientDeviceInformation devInfo = new EasClientDeviceInformation();
        public string GetModel()
        {
            return String.Format("{0} {1}", devInfo.SystemManufacturer, 
                devInfo.SystemProductName);
        }
        public string GetVersion()
        {
            return devInfo.OperatingSystem;
        }
    }
}
cs

PCL의 코드는 DependencyService 클래스를 사용하여 특정 플랫폼의 IPlatformInfo가 구현된 클래스에 액세스 할 수 있습니다. DependencyService는 세개의 public 메소드를 갖고 있는 static 클래스로서, 그 중 가장 중요한 메소드는 Get입니다. Get의 인자는 여러분이 작성한 인터페이스이고, 여기서는 IPlatformInfo입니다.

IPlatformInfo platformInfo = DependencyService.Get<IPlatformInfo>();
cs

Get 메소드는 IPlatformInfo 인터페이스를 구현하는 플랫폼별 클래스의 인스턴스를 반환합니다. 그런 다음 이 객체를 사용하여 플랫폼별 호출을 할 수 있습니다. 이 과정이 DisplayPlatformInfo 프로젝트의 code-behind 파일에 나와 있습니다.

namespace DisplayPlatformInfo
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            IPlatformInfo platformInfo = DependencyService.Get<IPlatformInfo>();
            modelLabel.Text = platformInfo.GetModel();
            versionLabel.Text = platformInfo.GetVersion();
        }
    }
}
cs

DependencyServiceGet 메서드를 통해 가져온 개체의 인스턴스를 캐시합니다. 이것은 Get의 후속 사용을 빠르게 하고, 인터페이스 플랫폼이 상태를 유지하도록 합니다. 플랫폼 구현의 모든 필드와 프로퍼티는 Get 호출을 여러번 해도 내내 보존됩니다. 이 클래스들에는 또한 이벤트와 콜백 메소드들이 들어 있습니다.

DependencyServicePlatInfoSap2 프로젝트에 시용된 접근 방식보다 약간의 오버 헤드가 필요하며, 개별 플랫폼 클래스가 공유 코드에 정의된 인터페이스를 구현하기 때문에 다소 구조가 복잡합니다.

DependencyService가 PCL에서 플랫폼별 호출을 구현하는 유일한 방법은 아닙니다. 모험을 좋아하는 개발자는 의존성 주입 기술(dependency-injection)을 사용하여 PCL이 플랫폼 프로젝트를 호출하도록 구성할 수도 있습니다. 하지만 DependencyService는 사용하기가 매우 쉬워서 Xamarin.Forms 앱을 만드는 데 SAP를 사용할 이유를 없애줍니다.


Platform-specific sound generation


이제 이법 챕터의 진짜 목표에 도달했습니다. MonkeyTap에 소리를 부여하는 것이죠. 세 플랫폼 모두 프로그램에서 오디오 파형을 동적으로 생성하고 재생할 수있는 API를 지원합니다. MonkeyTapWithSound 프로그램으로 이 목표를 이뤄보도록 합니다.

상업용 음악 파일은 종종 MP3 같은 형식으로 압축됩니다. 그러나 프로그램이 파형으로 알고리즘을 생성할 때는 비압축 형식이 훨씬 더 편리합니다. 세 가지 플랫폼 모두에서 지원되는 가장 기본적인 기술은 PCM(pulse code modulation)입니다. 거창한 이름에도 불구하고 매우 간단하며 음악 CD에 사운드를 저장하는데 사용되는 기술입니다.

PCM 파형은 샘플링 속도라고 부르는 일정한 속도를 가진 샘플들로 기술됩니다. 음악 CD는 초당 44,100 샘플의 표준 속도를 사용합니다. 컴퓨터 프로그램에서 생성된 오디오 파일은 고음질이 필요하지 않은 경우 절반(22,050) 또는 1/4 (11,025)의 샘플링 속도를 사용합니다. 기록 및 재생할 수있는 최고 주파수는 샘플링 속도의 절반입니다.

각 샘플은 한 시점에서 파형의 진폭을 의미합니다. 음악 CD의 샘플에는 16 비트 값이 적용됩니다. 음질이 그다지 중요하지 않을 때 8 비트 샘플이 일반적입니다. 일부 환경은 부동 소수점 값을 지원합니다. 여러 개의 샘플이 있는 경유 스테레오 또는 멀티채널을 수용할 수 있습니다. 그러나, 모바일 장치에서 간단한 사운드 효과를 얻으려면 모노 사운드가 좋을 수 있습니다.

MonkeyTapWithSound의 사운드 생성 알고리즘은 16 비트 모노 샘플로 하드 코딩되어 있지만 샘플링 속도는 상수로 지정되며 쉽게 변경할 수 있습니다.

이제 DependencyService가 어떻게 작동하는지 알기 때문에, MonkeyTapMonkeyTapWithSound로 바꾸기 위해 추가되는 코드를 위로부터 차근차근 살펴봅시다. 코드를 다시 쓰는 귀찮음을 피하기 위해 새 프로젝트에는 MonkeyTap 프로젝트의 MonkeyTap.xaml 및 MonkeyTap.xaml.cs 파일에 대한 링크를 포함 시키겠습니다.

Visual Studio에서는 프로젝트 메뉴의 추가> 기존 항목 추가를 선택하여 기존 파일에 대한 링크로 프로젝트에 항목을 추가 할 수 있습니다. 그리고 기존 항목 추가 대화창에서 파일을 찾고, 추가 버튼에서 드랍다운을 활성화시켜 링크로 추가를 선택합니다(여기서 사소한 문제가 있네요. 전에 생성한 프로젝트 이름이 책과는 달리 MainPage.xaml과 MainPage.xaml.cs로 생성되기 때문에 이름이 같아서 추가할 수가 없습니다. 링크로 추가시켜 보는 것도 하나의 연습이기 때문에 위의 프로그램 이름을 MonkeyTapPage.xaml과 MonkeyTapPage.xaml.cs로 바꿔서 진행해보도록 하겠습니다).

Xamarin Studio의 경우, 프로젝트 도구 메뉴에서 Add > Add Files를 선택하십시오. 파일을 열면 Add file to Folder 경고 상자가 나타납니다. Add a link to the file를 선택하십시오.

그러나 Visual Studio에서는 이 단계를 수행한 후 MonkeyTapWithSound.csproj 파일을 수동으로 편집하여(텍스트 편집기로 파일을 직접 열어 편집해 줍니다) MonkeyTapPage.xaml 파일을 EmbeddedResource로 변경하고 GeneratorMSBuild : UpdateDesignTimeXaml로 변경해야합니다. 또한 MonkeyTapPage.xaml 파일을 참조하기 위해 MonkeyTapPage.xaml.cs 파일에 DependentUpon 태그를 추가 시킵니다. 이렇게 해야 code-behind 파일이 파일 목록상에서 XAML 파일 아래에 들여 쓰기 됩니다.

그런 다음 MonkeyTapWithSoundPage 클래스는 MonkeyTapPage 클래스에서 파생됩니다. MonkeyTapPage 클래스는 XAML 파일과 code-behind 파일로 정의되지만 MonkeyTapWithSoundPage는 코드 전용입니다. 이런 방식으로 클래스가 파생될 때, XAML 파일의 이벤트에 대해서 원본 code-behind 파일의 이벤트 핸들러는 여기에서처럼 protected로 정의해야합니다.

MonkeyTap 클래스에서는 flashDuration 상수를 protected로, 두 개의 메서드는 protectedvirtual로 정의했습니다. MonkeyTapWithSoundPageSoundPlayer.PlaySound라는 정적 메서드를 호출하기 위해 다음 두 메서드를 오버라이드 합니다.

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

SoundPlayer.PlaySound 메서드는 빈도와 기간을 밀리초 단위로 허용합니다. 그 밖의 모든 것- 음량, 사운드의 조화, 사운드 생성 방법-은 PlaySound 메서드가 책임집니다. 그러나 이 코드는 SoundPlayer.PlaySound가 즉시 반환되고 사운드 재생이 완료될 때까지 기다리지 않는다는 암묵적인 가정하에 이루어 집니다. 다행히 세 개의 플랫폼 모두에서 사운드 재생 API는 그런 방식으로 작동합니다.

PlaySound 정적 메서드가있는 SoundPlayer 클래스는 MonkeyTapWithSound PCL 프로젝트의 일부입니다. 이 메서드는 사운드의 PCM 데이터 배열을 정의하는 역할을 합니다. 이 배열의 크기는 샘플링 속도와 지속 시간에 달려 있습니다. for 루프는 요청된 주파수의 삼각형 웨이브를 정의하는 샘플을 계산합니다(추가>새항목>클래스 선택하여 SoundPlayer를 솔루션에 추가했습니다):

using Xamarin.Forms;
namespace MonkeyTapWithSound
{
    class SoundPlayer
    {
        const int samplingRate = 22050;
        // Hard-coded for monaural, 16-bit-per-sample PCM
        public static void PlaySound(double frequency=440int duration = 250)
        {
            short[] shortBuffer = new short[samplingRate * duration / 1000];
            double angleIncrement = frequency / samplingRate;
            double angle = 0//normalize 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<IPlatformSoundPlayer>().PlaySound(samplingRate, byteBuffer);
        }
    }
}
cs

샘플은 16 비트 정수이지만, 플랫폼들 중 둘은 바이트 배열 형식의 데이터를 사용하기 때문에 Buffer.BlockCopy를 사용하여 변환합니다. 메서드의 마지막 행에서 DependencyService를 사용하여 이 바이트 배열을 샘플링 속도와 함께 개별 플랫폼에 전달합니다.

DependencyService.Get 메서드는 PlaySound 메서드의 시그니처를 정의하는 IPlatformSoundPlayer 인터페이스를 참조합니다.

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

지금부터가 어려운 부분입니다. 각각의 플랫폼에서 PlaySound 메소드를 작성할 것입니다.

iOS 버전은 AVAudioPlayer를 사용하는데, Waveform 형식(.wav) 파일을 사용하는 헤더가 포함되어야 합니다. 여기서 코드는 해당 데이터를 MemoryBuffer에 어셈블하고 이를 NSData 객체로 변환합니다.

using System;
using System.Text;
using System.IO;
using Xamarin.Forms;
using AVFoundation;
using Foundation;
[assembly:Dependency(typeof(MonkeyTapWithSound.iOS.PlatformSoundPlayer))]
namespace MonkeyTapWithSound.iOS
{
    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();
        }
    }
}
cs


핵심은 두가지 입니다. PlatformSoundPlayerIPlatformSoundPlayer 인터페이스를 구현하고, 클래스는 Dependency 특성으로 플래그가 지정됩니다.

안드로이드 버전은 AudioTrack 클래스를 사용하며, 조금 더 쉽습니다. 그러나 AudioTrack 객체는 오버랩을 허용하지 않으므로 이전 객체를 저장하고 다음 객체를 시작하기 전에 재생을 중지해야 합니다.

using System;
using Android.Media;
using Xamarin.Forms;
[assembly: Dependency(typeof(MonkeyTapWithSound.Droid.PlatformSoundPlayer))]
namespace MonkeyTapWithSound.Droid 
{
    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;
        }
    }
}
cs

Windows 플랫폼에서는 MediaStreamSource를 사용합니다. 반복적인 코드를 피하기 위해 MonkeyTapWithSound 솔루션에는 세 개의 플랫폼 모두에서 사용할 수 있는 클래스만으로 구성된 WinRuntimeShared라는 추가 SAP 프로젝트가 포함되어 있습니다(이제는 UWP만 남아 있어서 이런 파일이 존재하지 않습니다).

using System;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Media.Core;
using Windows.Media.MediaProperties;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Controls;
using Xamarin.Forms;
[assembly: Dependency(typeof(MonkeyTapWithSound.UWP.PlatformSoundPlayer))]
namespace MonkeyTapWithSound.UWP 
{
    class PlatformSoundPlayer : IPlatformSoundPlayer
    {
        MediaElement mediaElement = new MediaElement();
        TimeSpan duration;
        public void PlaySound(int samplingRate, byte[] pcmData)
        {
            AudioEncodingProperties audioProps = 
                AudioEncodingProperties.CreatePcm((uint)samplingRate, 116);
            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);
        }
    }
}
cs

(가뜩이나 파일이 여러 가지인데 템플릿이 변경되어 더더욱 복잡합니다. 자마린 깃헙에서 이 부분 참고 하셔서 잘 만들어 보시기 바랍니다.)

플랫폼 특정 작업을 수행하기 위해 DependencyService를 사용하는 것은 매우 강력하지만,이 접근법은 UI 요소에 관해서는 좀 부족합니다. Xamarin.Forms 응용 프로그램의 페이지를 장식하는 뷰를 확장해야 하는 경우, 이 책의 마지막 장에서 설명하는 플랫폼 별 렌더러를 만드는 작업이 필요합니다.

댓글 없음:

댓글 쓰기