wtorek, 31 maja 2016

[DSP2016] iOS prosto z poligonu odc.9 (próba audio w tle, z playlistą i wizualizacją FFT; CocoaPods; Swift + Objective-C/C)

Ten post będzie nieco inny od poprzednich. Do tej pory bywały dość szybkie piłki z od razu gotową implementacją. Tym razem będzie niemal na żywo o swoich zmaganiach z budową odtwarzacza audio w iOS połączonego z wizualizacją, co udało mi się dowiedzieć, co sprawdziłem, na jakie natrafiłem problemy… 

live

Co chcę mieć? To co wcześniej stworzyłem na Android w dość szybki i prosty sposób.  Chcę funkcjonalny odpowiednik systemowego odtwarzacza (odczytywanie z iPod Library, obsługa kolejki utworów, działanie w tle, sterowanie z lock screen) wzbogacony o możliwość analizy strumienia audio celem wizualizacji za pomocą FFT.

Na dzień dobry powiedzmy, jakie mamy zasadnicze możliwości odtwarzania audio w iOS. Są to:

  • systemowy odtwarzacz (używany przeze mnie w dotychczasowych źródłach, wysokopoziomowy full wypas)
  • wysokopoziomowe AVFoundation (AVAudioPlayer - dedykowany dla audio, ale bez kolejkowania utworów, AVPlayer - bardziej ogólny do audio i wideo, AVQueuePlayer - wersja AVPlayer z kolejkowaniem utworów)
  • Core Audio (mało przystępne niskopoziomowe API w C, dostęp do strumienia audio, możliwość analizy i przetwarzania, efektów itp.)
  • niskopoziomowe AVFoundation (AVAudioEngine, AVPlayerNode - opakowanie Core Audio w nowoczesną znacznie bardziej wygodną postać, nie zawsze satysfakcjonująca dokumentacja i przykłady)

W dokumentacji Apple warto rzucić okiem na Multimedia Programming Guide, AVFoundation Programming Guide czy Core Audio Overview. By robić wizualizację potrzebujemy dostępu do strumienia audio w trakcie jego odtwarzania, to jest możliwe albo za pomocą Core Audio albo za pomocą AVAudioEngine (pomijam tu proste wizualizacje w oparciu o siłę sygnału jak na stronie How To Make a Music Visualizer in iOS). A co Apple daje nam w zakresie transformaty FFT?  Mamy funkcje w C w ramach vDSP API w Acceleration Framework (są też oczywiście na świecie biblioteki, ale standardowe API może nam tutaj wystarczyć).

Eksperymenty zdecydowałem się przeprowadzać poza głównym projektem aplikacji iOS, na boku w osobnym “surowym” projekcie AudioPoligono (poligon doświadczalny). Po osiągnieciu stabilnego rozwiązania z założonymi funkcjonalnościami przeniosę go do aplikacji. 

Core Audio. Generalnie nie jest to bardzo przyjazne API, aczkolwiek po lekturze bardzo dobrej książki Learning Core Audio: A Hands-On Guide to Audio Programming for Mac and iOS przejaśni się co nieco w głowie i zobaczymy, że wilk nie jest taki straszny. W temacie przetwarzania FFT Apple oferuje całkiem niezły przykład aurioTouch, szkoda tylko że źródłem dźwięku jest w nim mikrofon, a nie plik audio. Jego nieoficjalną wersję w Swift znajdziemy pod adresem https://github.com/ooper-shlab/aurioTouch2.0-Swift.

Wilk jednak zawsze pozostanie wilkiem, nowoczesne AVAudioEngine było znacznie bardziej kuszące, więc praktyczną zabawę zacząłem od niego. Zachęciła mnie do tego prezentacja Building Modern Apps with AVAudioEngine. W efekcie powstała najpierw klasa CustomPlayer, a potem ją rozbudowałem do klasy CustomAudioPlayer, w której zmiksowałem obsługę kolejkowania wzorując się na samplu iOS Audio Player with Lock Screen Controls (gdzie playerem jest AVAudioPlayer). Metoda odtwarzająca wygląda tutaj tak:

func playItem(item: MPMediaItem) {
       
        guard let url = item.assetURL, let file = try? AVAudioFile(forReading: url) else {
            self.endPlayback()
            return
        }
               
        let audioEngine = AVAudioEngine()
        let audioPlayer = AVAudioPlayerNode()
       
        audioEngine.attachNode(audioPlayer)
        audioEngine.connect(audioPlayer, to: audioEngine.mainMixerNode, format: file.processingFormat)
       
        audioPlayer.scheduleFile(file, atTime: nil, completionHandler: {
            if self.nextItem == nil {
                self.endPlayback()
            }
            else {
                self.nextTrack()
            }
        })
        audioEngine.prepare()
       
        do {
            try audioEngine.start()
            audioPlayer.play()
           
            self.audioEngine = audioEngine
            self.audioPlayer = audioPlayer
           
            self.nowPlayingItem = item
           
            self.updateNowPlayingInfoForCurrentPlaybackItem()
           
            self.notifyOnTrackChanged()
           
        } catch {
            print("Error")
        }
    }

Na samym początku muzyka mi się nie odtwarzała. Okazało się, że system zwalniał obiekt odtwarzacza zanim rozpoczął granie, musiałem związać go z klasą (np.ViewController).

Jakimś cudem wpadłem na pomysł, by przekazać assetURL z MPMediaItem do AVAudioFile, co pozwoliło odtwarzać mi muzykę z iPod Library przez AVAudioEngine. Nie natrafiłem nigdzie na taki przykład, nawet było gdzieś o to pytanie na forum Apple’a i stackoverflow pozostawione bez odpowiedzi.

Zachęcony pierwszymi sukcesemi zrobiłem sobie audio w tle. Jak to się robi? Pomocna okazało się Q&A z dokumentacji (Playing media while in the background using AV Foundation on iOS) oraz kilka sampli (np. Playing audio in the background czy kapitalny iOS Audio Player with Lock Screen Controls). W capabilities projektu zaznaczamy pracę w tle i wybieramy audio. Dodatkowo w kodzie musimy ustawić sesję audio, bez tego to nie zadziała:

self.audioSession = AVAudioSession.sharedInstance()

try! self.audioSession.setCategory(AVAudioSessionCategoryPlayback)
try! self.audioSession.setActive(true)

W kodzie nie musimy nic robić, to używane przez nas API powoduje, że system podtrzymuje rozpoczętą aktywność odtwarzania po opuszczeniu foreground przez app-kę, a nawet po wyłączeniu ekranu telefonu. Kolejny sukces.

W dalszej kolejności zamierzałem skorzystać z tap-a na węźle końcowym, by pobierać co jakiś czas strumień audio trafiający do głośników, a później obrabiać go vDSP. Pomijając już niezbyt dobrą dokumentację (miałem kilka wpisów, jakiś slajd z prezentacji), o zaprzestaniu tej drogi zadecydowały pewne problemy z przejmowaniem odtwarzania audio, gdy inna aplikacja już coś odtwarzała. Taka obsługa wznawiania sesji jak w przypadku AVPlayer nie była możliwa, ale to w sumie detal. Najbardziej rozwalił mnie current time odtwarzacza i problem z prawidłową obsługą panelu odtwarzającego na lock-screen. AVAudioEngine nie ma takiego pola jak current time, które ma AVPlayer. Co prawda jest sposób, by sobie je obliczyć, jednak te wartości użyte do odświeżania informacji o aktualnie odtwarzanym utworze na lock-screen powodowały, że nie mogłem wznawiać odtwarzania po spauzowaniu, a czas leciał nawet po spauzowaniu. Co prawda znalazłem sposób, by go zatrzymać, ale powodowało to powrót do początku utworu.

Postanowiłem pójść w biblioteki. EZAudio wydawało się bardzo obiecujące. Nie dość, że mamy banalnie prostą obsługę odtwarzania, to jeszcze mamy też dość prosty przykład z analizą FFT:

5621705a-2971-11e5-88ed-9a865e422ade

Jak skorzystać z tej biblioteki?  Najprościej użyć CocoaPods, takiego odpowiednika nugeta. Polecam filmik, by zacząć go szybko używać.

Kolejnym problemem jest fakt, że EZAudio nie jest napisane w Swift, tylko w Objective-C. Ale problem nie okazał się duży. Apple bardzo elegancko to rozwiązał. Polecam szybką lekturę Using Swift with Cocoa and Objective-C (Swift 2.2). W jednym projekcie możemy mieć pliki zarówno w Swift, jak i w Objective-C. Mogą się wywoływać wzajemnie. Bajka. Tylko tworzymy plik pomostowy plik nagłówkowy .h z importem odpowiedniego nagłówka .h z Objective-C. XCode sam go zaproponuje stworzyć, jak zaczniemy tworzyć dowolny plik Objective-C w projekcie Swift (plik Objective-C możemy potem wyrzucić). W ten sposób stworzyłem plik AudioPoligono-Bridging-Header.h , którego treść zmodyfikowałem do postaci:

#import <EZAudio/EZAudio.h>

W źródłach EZAudio 1.4 był problem i zgodnie z wątkiem na github poprawiłem tam import w dwóch plikach .h. Zobaczyłem BTW że jest już wersja 1.5 i może ją wypróbuję następnym razem (1.4 zainstalowałem kierując się dokumentacją na głównej stronie github).  Dla porównania przykładowy projekt będący taką integracją, ale sprzed 11 miesięcy możemy znaleźć na https://github.com/syedhali/EZAudio-Swift.

Stworzyłem kod odtwarzający audio (aha, aby używać obiektów z biblioteki dodałem jeszcze u góry pliku:  import EZAudio):

func playItem(item: MPMediaItem) {       
      
       let file = EZAudioFile(URL: item.assetURL)
      
       let player = EZAudioPlayer(delegate: self)
      
       player.playAudioFile(file)
      
       self.audioPlayer = player
      
       self.sampleRate = Float(file.clientFormat.mSampleRate)
       self.fft = EZAudioFFTRolling(windowSize: FFTWindowSize, sampleRate: self.sampleRate, delegate: self)
      
       self.nowPlayingItem = item
      
       self.updateNowPlayingInfoForCurrentPlaybackItem()
      
       self.notifyOnTrackChanged()
   }

Okazało się jednak, że oryginalne EZAudio (przynajmniej to 1.4) rzuca błedem przy przekazaniu do EZAudioFile (czy EZAudioPlayer) url-a z iPod Library. Lektura wątku na github spowodowała, że wgrałem zapronowaną przez kogoś poprawkę warunku i odtwarzanie ruszyło. Hurra!

Odkryłem jednak, że EZAudio zawiesza odtwarzanie, jak wyłączy się ekran telefonu. Jak się okazało na kolejnym wątku w github, to znany problem. Tymczasowo można rozwiązać go poprzez ustawienie rodzaju sesji na odtwarzanie i nagrywanie, zamiast tylko na odtwarzanie. Dało to oczekiwany efekt. Zauważyłem jednak, że czasami odtwarzanie na malutką chwilkę może spowodować lekką chrypkę podczas przechodzenia aplikacji do background. Nie zawsze, ale może zdarzyć się. Dochodzi do tego fakt, że dotychczasowa implementacja kolejkowania utworów, o ile dobrze działała w tle w poprzednim przypadku z AVFoundation, o tyle w EZAudio następuje co prawda przejście na następnego kawałka, ale po kilku sekundach odtwarzacz milknie. Plusem natomiast jest to, że EZAudio ma current time tak jak AVAudioPlayer, co daje nam poprawne sterowanie odtwarzaniem z poziomu lock-screen. Mogę pauzować, wznawiać, przechodzić między utworami bez żadnych problemów, czas dobrze się pokazuje i zatrzymuje kiedy potrzeba.

Pewne problemy z EZAudio może byłbym w stanie jakoś przeboleć (i szukać nieco później dla nich rozwiazania nawet analizując kod EZAudio), by cieszyć się wizualizacją FFT. Tutaj sukces jest jednak tylko częściowy. Co prawda implementując metodę protokołu EZAudioPlayerDelegate:

public func audioPlayer(audioPlayer: EZAudioPlayer!, playedAudio buffer: UnsafeMutablePointer<UnsafeMutablePointer<Float>>, withBufferSize bufferSize: UInt32, withNumberOfChannels  numberOfChannels: UInt32, inAudioFile audioFile: EZAudioFile!) {
       
        self.fft.computeFFTWithBuffer(buffer[0], withBufferSize: bufferSize)
    }

i protokołu EZAudioFFTDelegate:

public func fft(fft: EZAudioFFT!, updatedWithFFTData fftData: UnsafeMutablePointer<Float>, bufferSize: vDSP_Length) { … }

udało mi się podczas odtwarzania dostawać regularnie wyliczane tablice float-ów, ale czasami przy starcie odtwarzania czy tuż po jego zakończeniu miałem EXC_BAD_ACCESS na UnsafeMutablePointer<Float>, który jest odpowiednikiem tablicy z Objective-C w Swift. To powinno dać się poprawić. Sporej poprawki z pewnością wymaga jednak logika przetwarzajaca dane otrzymywane w ostatniej metodzie. Próba adaptacji kodu z Androida, gdzie otrzymywałem bajty zamiast floatów po uwzględnieniu tego faktu nie daje w pełni satysfakcjonujących wyników. Na kanale basu mam w miarę sensowne wartości od 0 do 1, o tyle w kanale środkowym to Nan, a tony wysokie zawsze oscylują przy zerze. Może winna jest klasa EZAudioFFTRolling używana w wykresach i może zamiast niej bezpośrednio korzystać z EZAudioFFT czy systemowego vDSP?

A może znajdzie są coś lepszego i bardziej niezawodnego od EZAudio? W każdym razie ta tematyka w iOS nie jest tak prosta jak w przypadku Androida, gdzie mamy bardzo dobry przykład od Google i gotowe wsparcie dla FFT przy odtwarzaniu na poziomie systemowego API.

Myślę, że to zagadnienie będę dalej kontynuować, już poza Daj Się Poznać 2016, bo jesteśmy na finishu tej zacnej inicjatywy. To oczywiście niejedyna rzecz, jaką zajmiemy się w okresie post-DSP –Winking smile

sobota, 28 maja 2016

BUILD 2016 - odc.6 (Visual Studio, Continuum, IoT)

Mała przerwa w DSP by powrócić do BUILD’a.  5 filmików. Muszę przyznać, że spodziewałem się po nich znacznie więcej, a tymczasem dostałem dość przewidywalne treści, w części będącego powtórką tego, co już wiem. Może to kwestia, że znowu wybrałem tematy związane z Windows 10, UAP, Continuum, IoT?  W każdym razie podsumujmy dzisiejszą kinematografię:

  • co nowego w Visual Studio dla aplikacji uniwersalnych - nie czuję jakiegoś wielkiego przełomu, po raz kolejny edycja XAML w runtime, znajdziemy jednak trochę różnych drobiazgów, z których się ucieszymy
  • IoT - pomału do przodu, zgrabne podsumowanie przedsięwzięć z ostatnich miesięcy związanych z Windows 10 IoT Core, te wszystkie pakiety, zestawy ułatwią życie, kapitalny zdalny klient, teraz nie trzeba podłączać monitora/wyświetlacza do płytki by zobaczyć z niej obraz. Poza tym są jeszcze inne edycje Windows IoT, o których mało się mówi, a one też mogą uczestniczyć w naszym życiu. Pamiętajmy o OBD2 w samochodzie, poprzez bezprzewodowy adapter możemy zintegrować z nim nasze mobilne aplikacje.
  • Continuum dla telefonu z Windows - zacna idea, w analizie skupiłem się na aspektach sprzętowych, w części soft zasadniczo nic nowego

 

What’s New in Visual Studio for Universal Windows App Development

image

image

image

image

image

wykrywanie w edytorze VS wywołań API specyficznego dla danego rodzaju Windows (desktop, mobile, itp.), może wstawić też odpowiedniego if-a

image

image

image

VS stał się mądrzejrzy…

image

W intellisense XAML pokazywane elementy jeszcze przed zaimportowaniem do nich namespace’u

Blend - wyszukiwarka w panelu Resource

Packaging Wizard - usprawnienia przy publikacji do Windows Store

image

image

 

Windows 10 IoT Core: From Maker to Market

image

image

image

aplikacja foreground + kontroler

image

serwis + kontroler

image

komunikacja aplikacji foreground z serwisem

image

image

łatwe sterowanie popularnymi elementami

image

image

image

image

image

VB

C++

image

image

image

image

image

wszystkie urządzenia: PC, telefony, …

image

a teraz sobie posterujmy kołem z desktopowego Windows 10 przez zdalnego klienta

image

image

image

image

image

 

Continuum for Phone: Optimizing Windows Apps Across Screens

image

łączność przewodowa lub bezprzewodowa

dzięki Azure Remote App można pracować na dużym ekranie z Visual Studio za pośrednictwem telefonu

image

telefon jako zdalny touch pad i klawiatura

image

Można też podłączyć bezprzewodowo telefon bezpośrednio do monitora wyposażonego w bezprzewodowy klucz

image

Clamshell

klawiatura i monitor, wyglądające jak laptop, napędzane przez telefon

HP Elite x3 

image

image

image

 

Microsoft Vision for IoT: From Windows Devices to Azure

image

aplikacja Web w Raspberry Pi

image

image

W przemyśle Windows 10 pójdzie lepiej?

image

image

image

Bluetooth LE beacon z Windows 10 IoT Core

 

Deep Dive Into IOT Starter Kit App: Architecture and Getting Started on Building Your IOT Solution

image

integracja z OBD2 (dane samochodowe np. prędkość, spalanie) 

iPhone, Android, UWP

adapter OBD z Wi-Fi

image