Windows Phone: разное

Фоновое воспроизведение аудиозаписей в Windows Phone 7.5

Чарльз Петцольд

Исходный код можно скачать по ссылке code

Charles PetzoldВ стародавние времена MS-DOS программисты могли реализовать грубую форму переключения между задачами с помощью методики «Terminate and Stay Resident» (TSR) (резидентные программы). TSR-программы устанавливали ловушки для перехвата прерываний от клавиатуры или использовали другие механизмы операционной системы, а затем завершались, но программа оставалась в памяти и была готова отреагировать на нажатие пользователем определенного сочетания клавиш или при другом событии.

MS-DOS не была рассчитана на обработку даже такого рудиментарного уровня переключения задач, поэтому резидентные программы вызывали серьезные проблемы. Они могли конфликтовать друг с другом и часто приводили к краху всей ОС. Это было одной из основных причин, почему некоторые из нас столь радостно приветствовали Windows, которая поначалу дополняла MS-DOS, а потом полностью ее заменила..

Я вспомнил античную историю, потому что намерен показать вам, как воспроизводить музыкальные файлы в фоне из приложения Windows Phone 7.5, и мне известно, что среди разработчиков наблюдается тенденция к поиску нетривиальных решений. В целом, это хорошо, но вы не должны использовать этот прием для иных целей, кроме проигрывания музыкальных файлов. Иначе ваше приложение может быть отклонено в Windows Phone Marketplace.

Методика, которую я продемонстрирую, предназначена только для воспроизведения звуковых или музыкальных файлов из Интернета или изолированного хранилища. Если вашему приложению нужно воспроизводить песни из обычной музыкальной библиотеки в Phone, то вы можете делать это с помощью классов MediaLibrary и MediaPlayer, о которых мы говорили в прошлой статье (msdn.microsoft.com/magazine/hh708760).

Приложение плюс DLL

В Windows Phone 7.5 объект, выполняемый в фоне и являющийся вспомогательным для приложения, называют агентом. Чтобы воспроизводить музыкальные файлы в фоне, вы используете какой-нибудь класс, производный от AudioPlayerAgent. Этот класс должен находиться в DLL, на которую ссылается ваша программа. В фоне выполняется не код самой программы, а этот класс, производный от AudioPlayerAgent.

В пакет исходного кода для этой статьи входит решение Visual Studio под названием SimpleBackgroundAudio. Для большей ясности эта программа содержит лишь минимальный объем кода, необходимый для того, чтобы работало фоновое воспроизведение аудио. Я создал это решение в диалоговом окне New Project, указав Windows Phone Application, а затем Windows Phone 7.1. (Обозначение 7.1 используется как внутренняя версия Windows Phone ОС и приложений Windows Phone, но оно равнозначно более часто используемому обозначению 7.5.)

В класс MainPage проекта SimpleBackgroundAudio я добавил Button и обработчик событий Click для этой кнопки:

void OnPlayButtonClick(object sender, RoutedEventArgs args)
{
  AudioTrack audioTrack = 
    new AudioTrack(new Uri("http://www.archive.org/.../Iv.Presto.mp3"), 
                   "Symphony No. 9: 4th Movement", 
                   "Felix Weingartner", 
                   "Beethoven: Symphony No. 9", 
                   null, 
                   null, 
                   EnabledPlayerControls.Pause);
  BackgroundAudioPlayer.Instance.Track = audioTrack;
}

Классы AudioTrack и BackgroundAudioPlayer находятся в пространстве имен Microsoft.Phone.BackgroundAudio. URL (который я сократил здесь, чтобы он уместился по ширине колонки) ссылается на MP3-файл в Internet Archive (archive.org). Он содержит последнюю часть 9-й симфонии Бетховена, исполняемой оркестром под управлением дирижера Феликса Уейнгартнера (Felix Weingartner); это запись 1935 года. (В качестве альтернативы можно указать URL, ссылающийся на файл в изолированном хранилище; тогда используйте аргумент UriKind.Relative в конструкторе Uri.) Следующие три аргумента конструктора AudioTrack предоставляют информацию об альбоме, исполнителе и треке.

Класс BackgroundAudioPlayer чем-то напоминает класс MediaElement или MediaPlayer, который воспроизводит аудиофайлы в фоновом режиме. В BackgroundAudioPlayer нет конструктора; вместо этого вы получаете единственный экземпляр со статическим свойством Instance. В этом коде в свойстве Track указывается только что созданный объект AudioTrack.

Вы можете скомпилировать и запустить эту программу, но она не будет ничего делать. Чтобы BackgroundAudioPlayer «запел», вам потребуется DLL, содержащая класс, производный от AudioPlayerAgent. Visual Studio может создать этот класс и проект библиотеки за вас. В своей программе я щелкнул правой кнопкой мыши решение SimpleBackgroundAudio в Visual Studio, выбрал Add New Project из меню, а затем в списке шаблонов указал Windows Phone Audio Playback Agent. Этот новый проект я назвал SimpleAudioPlaybackAgent.

Visual Studio создает проект библиотеки с классом AudioPlayer, производным от AudioPlayerAgent и инициализируемым с помощью нескольких методов-заготовок. Это класс, выполняемый в фоновом режиме.

Очень важно! Создайте ссылку из приложения на эту DLL! Для этого щелкните правой кнопкой мыши раздел References в узле проекта SimpleBackgroundAudio, выберите Add Reference, в появившемся диалоговом окне откройте вкладку Projects, а затем проект SimpleAudioPlaybackAgent.

В производном от AudioPlayerAgent классе вы захотите модифицировать минимум два метода: OnPlayStateChanged и OnUserAction. Полный исходный код класса AudioPlayer из этого проекта (кроме директив и комментариев) показан на рис. 1..

Рис. 1. Класс AudioPlayer в SimpleBackgroundAudio

namespace SimpleAudioPlaybackAgent
{
  public class AudioPlayer : AudioPlayerAgent
  {
    protected override void OnPlayStateChanged(BackgroundAudioPlayer player,
                                      AudioTrack track, PlayState playState)
    {
      switch (playState)
      {
        case PlayState.TrackReady:
          player.Play();
          break;

        case PlayState.TrackEnded:
          player.Track = null;
          break;
      }
      NotifyComplete();
    }
    protected override void OnUserAction(BackgroundAudioPlayer player, 
                                      AudioTrack track, UserAction action, 
                                      object param)
    {
      switch (action)
      {
        case UserAction.Pause:
          player.Pause();
          break;

        case UserAction.Play:
          player.Play();
          break;
      }
      NotifyComplete();
    }

    protected override void OnError(BackgroundAudioPlayer player, 
                                      AudioTrack track, Exception error, 
                                      bool isFatal)
    {
        base.OnError(player, track, error, isFatal);
        NotifyComplete();
    }
    protected override void OnCancel()
    {
        base.OnCancel();
        NotifyComplete();
    }
  }
}

Взгляните сначала на переопределенную версию OnPlayStateChanged. Этот метод вызывается, когда изменяется свойство PlayState в BackgroundAudioPlayer. Первый аргумент — тот же BackgroundAudioPlayer, на который ссылается код программы, а последний аргумент — член перечисления PlayState.

Когда программа задает свойство Track объекта BackgroundAudioPlayer в обработчике событий Click, BackgroundAudioPlayer обращается к музыкальному файлу по Интернету; далее загружается SimpleAudioPlaybackAgent DLL, и в конечном счете вызывается метод OnPlayStateChanged с аргументом playState, установленным в PlayState.TrackReady. Вызов метода Play объекта BackgroundAudioPlayer для начала воспроизведения этого трека возлагается на OnPlayStateChanged.

Если в этот момент что-то воспроизводится стандартным музыкальным проигрывателем, он будет остановлен. После этого вы можете переключиться из этой программы куда-то еще или даже завершить ее нажатием кнопки Back, а музыка продолжит проигрываться.

Вы можете запустить эту программу в эмуляторе Windows Phone, но лучше опробовать ее на реальном устройстве, чтобы у вас появилась возможность использовать кнопку громкости звука. Нажатие на нее приводит к появлению небольшой выпадающей панели под названием Universal Volume Control (UVC), которая является важным элементом в фоновом воспроизведении аудио. UVC отображает название проигрываемого трека и имя исполнителя на основе аргументов, переданных приложением в конструктор AudioTrack. UVC также выводит кнопки Previous Track, Pause и Next Track. В этой программе активна лишь кнопка Pause, так как именно это я указал в последнем аргументе для конструктора AudioTrack. При нажатии кнопки Pause происходит переключение между Pause и Play.

Класс AudioPlayer обрабатывает эти команды Pause и Play в переопределенной версии OnUserAction, показанной на рис. 1. Аргумент UserAction указывает конкретную кнопку, нажатую пользователем. OnUserAction реагирует на это вызовом соответствующего метода в объекте BackgroundAudioPlayer.

Когда проигрывание трека заканчивается, вызывается OnPlayStateChanged с передачей PlayState.TrackEnded. Этот метод реагирует на вызов установкой свойства Track объекта BackgroundAudioPlayer в null, что приводит к удалению элемента из UVC. Если хотите, то можете вернуться в приложение и снова запустить воспроизведение музыки.

Заметьте, что и OnPlayStateChanged, и OnUserAction заканчивают вызовом NotifyComplete: это обязательно. Также обратите внимание, что ни один из методов не включает вызов метода базового класса. Эти вызовы базового класса являются частью шаблона, создаваемого Visual Studio за вас, но у меня была проблема с переопределенной версией OnUserAction, когда вызывался метод базового класса. В примере кода для фонового воспроизведения аудио от Microsoft (этот пример доступен в документации Windows Phone 7.5) тоже нет вызовов базовых методов.

Очень странная DLL

Экспериментируя с фоновым воспроизведением аудио, понаблюдайте за содержимым окна Output в Visual Studio. Когда SimpleBackgroundAudio запускается из отладчика, в окне Output перечисляются все системные библиотеки, загруженные на устройство Phone для выполнения программы, а также сама программа, каковой является SimpleBackgroundAudio.dll. Строки в окне Output, где перечисляются эти библиотеки, начинаются со слов «UI Task», указывающих на эту программу.

Коснувшись кнопки в SimpleBackgroundAudio, вы видите, что загружаются многие из тех же DLL, но теперь каждая строка предваряется словами «Background Task». Эти DLL включают DLL приложения и SimpleAudioPlaybackAgent.dll. Загрузка этих DLL — одна из причин некоторой задержки до начала воспроизведения музыки после того, как вы касаетесь кнопки.

При экспериментах с программами, которые воспроизводят аудио в фоновом режиме, вы, вероятно, захотите вставить выражения Debug.WriteLine во всех переопределенные методы класса AudioPlayer, а затем изучить их поведение в реальном времени в окне Output.

Возможно, вы также захотите создать конструктор для класса AudioPlayer с другим выражением Debug.WriteLine. Вы обнаружите, что новый экземпляр AudioPlayer создается всякий раз, когда возникает необходимость в вызове OnPlayStateChanged или OnUserAction. Каждый вызов приводит к созданию нового экземпляра!

Этот простой факт влечет за собой глубокие последствия: если этот класс AudioPlayer необходим вам для сохранения информации между вызовами OnPlayStateChanged и OnUserAction, вы должны записывать ее в статические поля или свойства.

А как быть, если нужно передать информацию от какого-либо класса в программе SimpleBackgroundAudio классу AudioPlayer в библиотеке SimpleAudioPlaybackAgent? Кажется логичным определить открытый статический метод в классе AudioPlayer, а затем вызывать этот метод из MainPage или другого класса в программе. Вы действительно может сделать так, но это не даст вам того, что вы хотели: вся сохраненная информация, получаемая от этого метода, будет недоступна при вызовах OnPlayStateChanged и OnUserAction.

Почему же она оказывается недоступной? Вспомните обозначения «UI Task» и «Background Task» в окне Output в Visual Studio. Эта два разных процесса. Экземпляр DLL, на который ссылается ваша программа, — это вовсе не тот экземпляр, который выполняется в фоне, и поэтому даже статическими данными в каком-либо классе из этой DLL нельзя обмениваться между UI Task и Background Task.

Тестируя приложение для фонового воспроизведения аудио из отладчика в Visual Studio, вы заметите еще несколько странностей. При выходе из программы, инициировавшей фоновое воспроизведение звука, этот звук продолжает проигрываться, и Visual Studio показывает, что данный код все еще выполняется. Чтобы возобновить редактирование своей программы, вам потребуется остановить отладку непосредственно в Visual Studio, и, даже когда вы это сделаете, зачастую музыка будет продолжать играть. В процессе разработки программы фонового воспроизведения аудио вам скорее всего придется довольно часто удалять свою программу с устройства Phone.

Расширение приложения

В программе SimpleBackgroundAudio есть одна крупная проблема. Хотя в ней имеется кнопка для запуска проигрывания музыки, нет никакого способа приостановить ее или выключить. Программе даже не известно, когда закончится музыка или произойдет нечто другое. Да, пользователь всегда может вызвать UVC для управления фоновой музыкой, но любая программа, которая запускает воспроизведение звука, должна иметь и свои средства управления.

Эти усовершенствования вошли в проект Playlist¬Player. Как и предполагает его название, программа проигрывает серию последовательных треков — в данном случае 12 небольших фортепианных частей из сочинения Клода Дебюсси «Прелюдии, книга первая», исполняемых Альфредом Корто (Alfred Cortot), в записи 1949 года, доступной из Internet Archive.

Изначально я хотел создавать все объекты AudioTrack внутри программы, а затем передавать их DLL фонового воспроизведения аудио. Мне казалось, что у программы получится более универсальная структура, но я обнаружил, что такой вариант не работает из-за того, что приложение обращается к другому экземпляру DLL — не тому, который выполняется в фоновом режиме. Тогда я написал класс AudioPlayer, который создает все объекты AudioTrack и хранит их в обобщенном List — списке воспроизведения (playlist).

Чтобы несколько усложнить программу, я решил, что этот список не должен быть круговым: я не хотел перехода от последнего трека к первому или от первого к последнему. По этой причине в первом из конструкторов AudioTrack последний аргумент был комбинацией флагов EnabledPlayerControls.Pause и EnabledPlayerControls.SkipNext, а конструктор последнего AudioTrack комбинировал флаги Pause и SkipPrevious. У всех остальных были все три флага. Именно так в UVC активизуются соответствующие три кнопки для различных треков.

На рис. 2 показаны переопределенные версии OnPlayStateChanged и OnUserAction. В OnPlayStateChanged, когда один из треков заканчивается, следующий трек устанавливается в соответствии со значением свойства Track объекта BackgroundAudioPlayer. Переопределенная версия OnUserAction обрабатывает команды Previous Track и Next Track от UVC, присваивая свойству Track предыдущий или следующий AudioTrack в списке воспроизведения.

Рис. 2. Переопределенные методы объекта AudioPlayer в PlaylistPlayer

static List<AudioTrack> playlist = new List<AudioTrack>();
static int currentTrack = 0;

...

protected override void OnPlayStateChanged(BackgroundAudioPlayer player, 
  AudioTrack track, PlayState playState)
{
  switch (playState)
  {
    case PlayState.TrackReady:
      player.Play();
      break;

    case PlayState.TrackEnded:
      if (currentTrack < playlist.Count - 1)
      {
        currentTrack += 1;
        player.Track = playlist[currentTrack];
      }
      else
      {
        player.Track = null;
      }
      break;
  }
  NotifyComplete();
}

protected override void OnUserAction(BackgroundAudioPlayer player, 
  AudioTrack track, UserAction action, object param)
{
  switch (action)
  {
    case UserAction.Play:
      if (player.Track == null)
      {
        currentTrack = 0;
        player.Track = playlist[currentTrack];
      }
      else
      {
        player.Play();
      }
      break;

    case UserAction.Pause:
      player.Pause();
      break;

    case UserAction.SkipNext:
      if (currentTrack < playlist.Count - 1)
      {
        currentTrack += 1;
        player.Track = playlist[currentTrack];
      }
      else
      {
        player.Track = null;
      }
      break;

    case UserAction.SkipPrevious:
      if (currentTrack > 0)
      {
        currentTrack -= 1;
        player.Track = playlist[currentTrack];
      }
      else
      {
        player.Track = null;
      }
      break;

    case UserAction.Seek:
      player.Position = (TimeSpan)param;
      break;
  }
  NotifyComplete();
}

TВ классе MainPage программы имеется набор из четырех стандартных кнопок для панели приложения, выполняющих те же функции, что и UVC. Он также задает обработчик событий PlayStateChanged объекта BackgroundAudioPlayer для обновления экрана информацией о текущем треке и обработчик CompositionTarget.Rendering для обновления Slider текущей позицией в треке, как показано на рис. 3.

Рис. 3. Программа PlaylistPlayer

Программа PlaylistPlayer

Логика включения/отключения кнопок на панели приложения довольно проста: кнопки Previous Track и Next Track становятся доступными в зависимости от значения свойства PlayerControls текущего AudioTrack; благодаря этому они должны быть согласованы с UVC. Кнопка Pause становится доступной, если проигрыватель воспроизводит музыку, а кнопка Play — если проигрыватель поставлен на паузу. Если текущий трек — null, Play становится доступной, а все остальные кнопки отключаются.

Обработчики событий Click четырех кнопок на панели приложения вызывают соответственно методы SkipPrevious, Play, Pause и SkipNext объекта BackgroundAudioPlayer. Важно понимать, что эти вызовы не управляют воспроизведением музыки напрямую. Вместо этого они инициируют вызовы OnUserAction в классе AudioPlayer, и именно код AudioPlayer реально запускает и останавливает проигрывание музыки.

Это несколько странно, так как получается, что вызовы методов Play и Pause объекта BackgroundAudioPlayer ведут себя по-разному в зависимости от того, откуда они инициируются — из программы или из переопределенной версии OnUserAction.

Я также добавил обработчик ValueChanged для Slider, чтобы можно было переходить в определенную позицию в треке. Этот обработчик вычисляет новую позицию и присваивает соответствующий объект TimeSpan свойству Position объекта BackgroundAudioPlayer. По аналогии с вызовами Play и Pause, задание этого свойства не изменяет позицию в треке. Вместо этого генерируется вызов переопределенной версии OnUserAction в AudioPlayer с аргументом action, равным UserAction.Seek, и TimeSpan, закодированным в аргументе param. Затем переопределенная версия OnUserAction присваивает этот TimeSpan свойству Position в BackgroundAudioPlayer, и только потом происходит реальная смена позиции в треке.

На практике этот Slider работает нормально, когда вы просто касаетесь его для перемещения по треку вперед или назад на 10%. Если вы пытаетесь сместить ползунок, то, судя по всему, формируется пакет вызовов Seek, и в результате слышна мешанина звуков. Я предпочел бы использовать обычный ScrollBar вместо Slider, потому что тогда я смог бы ждать события EndScroll, которое генерируется, когда пользователь прекращает манипуляции с этим элементом управления. К сожалению, мне так и не удалось уговорить ScrollBar хоть как-то работать в Windows Phone.

Ограничения

Было интересно посмотреть, как Windows Phone 7.5 обеспечивает программистам более прямой доступ к аппаратным средствам (например, к видеоканалу), а также дает возможность выполнять некоторые фоновые операции. Но мне не дает покоя мысль, что в этой реализации фонового воспроизведения аудио не хватает какой-то части.

Допустим, программа должна поддерживать выбор пользователя из списка набора музыкальных файлов, которые требуется воспроизводить последовательно. Программа не может передать весь список в DLL, поэтому ей придется брать на себя заботу об установке свойства Track в BackgroundAudioPlayer по окончании каждого трека. Но это возможно, только если программа работает не в фоновом режиме.

Можно передавать кое-какую информацию между приложением и фоновой DLL через строковое свойство Tag класса AudioTrack. Но не тратьте время на создание класса, производного от AudioTrack, в надежде добиться передачи дополнительной информации DLL: для передачи в переопределенные методы DLL приложением создается копия объекта AudioTrack.

К счастью, эта DLL имеет доступ к той части изолированного хранилища, которая отведена приложению, поэтому программа может сохранить там небольшой файл, определяющий список воспроизведения, а в дальнейшем DLL сможет считывать этот список из файла. Бонусная программа для этой статьи — проект под названием PlaylistFilePlayer, который демонстрирует простой способ реализации этого подхода.

Исходный код можно скачать по ссылкеcode.msdn.microsoft.com/mag201202TouchNGo.

Чарльз Петцольд  (Charles Petzold) — давний «пишущий» редактор MSDN Magazine. Его последнюю книгу «Programming Windows Phone 7» (Microsoft Press, 2010) можно скачать бесплатно по ссылке bit.ly/cpebookpdf.

Выражаю благодарность за рецензирование статьи эксперту: Марку Хопкинсу (Mark Hopkins).