Làm việc với Audio trong iOS và những lời khuyên cho developer (Phần 1)

Toàn bộ dữ bài viết của tôi tại Viblo đã được chuyển về blog cá nhân tại HuyPham.com

Blog đang được gấp rút hoàn thiện và còn một số lỗi hiển thị, cám ơn các bạn đã theo dõi và ủng hộ.

Link mới: Làm việc với Audio trong iOS và những lời khuyên cho developer (Phần 1)

Hiểu bài toán của mình

Điều đầu tiên và quan trọng nhất đó là bạn cần hiểu các yêu cầu của ứng dụng khi tương tác với Audio. Bạn sẽ cần phải trả lời một số câu hỏi sau đây để có thể estimate một cách chuẩn xác cho ứng dụng sử dụng Audio của mình, và nhiều khi, khách hàng cũng chẳng rõ specs cho lắm cho tới khi bạn Q&A.

Câu hỏi hẳn là các bạn sẽ có sẵn lời giải đáp, đó là: Ứng dụng play hay ghi âm, hay cả hai.

Ứng dụng có play audio

  • Ứng dụng load audio từ đâu? (iOS, local app hay từ server trên mạng)
  • Audio được sử dụng để làm gì? (sound effect, background music, music)
  • Ứng dụng có cho phép âm thanh của các ứng dụng khác (như khi người dùng bật audio từ iTunes, Zing Mp3, Spotify, Podcast,…) được play cùng không?
  • Người dùng có thể tương tác với audio không, nếu có thì làm được những gì? (play/pause, next/back, đổi tốc độ play, đổi volume,…)
  • Nút bấm tai nghe có được phép hoạt động không, cần xử lý các event nào (toggle play/pause, next/back,…)
  • Có hiển thị Media Player ở màn hình khóa không, nếu có thì những thông tin nào được show ra và các nút tương tác nào user có thể sử dụng.
  • Khi cắm, rút tai nghe, hành vi của app sẽ thực hiện ra sao?
  • Các hành vi cụ thể của việc play âm thanh sẽ ra sao khi cắm, rút tai nghe hoặc khi bị ngắt bởi các ứng dụng khác hay cuộc gọi tới?

Ứng dụng có ghi âm

  • Ứng dụng có thể cắt audio, ghép audio hay không?
  • Ứng dụng có yêu cầu vẽ sound wave không?
  • Nếu có vẽ sound wave, animation của sound wave chạy thế nào? (Điều này ảnh hưởng đến chu kì nhả data ghi âm vào buffer nên clear phần này là rất quan trọng nhé)
  • Có nén dữ liệu sau khi ghi âm không, nếu có, định dạng nén là gì?
  • Có cần khôi phục và ghi tiếp khi app bị kill đột ngột hay không?
  • Các hành vi cụ thể của việc ghi âm sẽ ra sao khi cắm, rút tai nghe hoặc khi bị ngắt bởi các ứng dụng khác hay cuộc gọi tới?

Nói chung mà nói, ghi âm sẽ khó hơn, bạn cần tìm hiểu nhiều concept về media hơn, thậm chí, nếu yêu cầu phức tạp, bạn có thể sẽ phải xuống tới tầng Core Audio và làm việc với các thư viện C, phải hiểu và tự setup các cổng input/output, tự xử lý buffer,… và tốt nhất là bạn nên đẩy lên tầng AVFoundation càng nhanh càng tốt.

Sẽ còn nhiều những bài toán có những yêu cầu đặc biệt hơn dựa trên specs của ứng dụng. Do đó, các bạn cần hiểu đây là các câu hỏi tham khảo, và bạn có thể mở rộng nó ra nhé!

Hiểu các lớp tương tác Audio trong AVFoundation

Trước tiên phải nói AVFoundation là gì? Nói vậy chứ ông nào định làm việc với audio mà chả nghe về nó rồi. Nhưng để mà hiểu nó hơn thì chúng ta sẽ đi phân tích lại nhé!

Ngày xửa ngày xưa, khi chưa có AVFoundation (mà ngay cả lúc có rồi nhưng vẫn còn bug lòi ra), các developer muốn phát triển ứng dụng tương tác với audio cần phải làm việc ở tầng Core Audio, code bằng C/C++, và phải xử lý rất là lắm thứ liên quan kể cả việc thiết lập điều khiển phần cứng.

Tuy nhiên, Apple đã phát triển AVFoundation, kiến tạo các class dùng chung, xử lý hết mớ lằng nhằng mà ta phải implemtent hết lần này lần khác với Core Audio, và xuất ra một Objective-C interface để ta có thể sử dụng bằng Objective-C cho nó tiện.

Vâng đó là AVFoundation framework, phải công nhận rằng nó rất tiện. Play audio hay ghi âm chỉ cần một đoạn code ngắn rất clear và dễ hiểu.

Vậy thì cuối cùng, AVFoundation có gì để trở nên bá đạo như vậy?

AVAudioSession

Nếu bạn chưa biết, thì mỗi ứng dụng trong hệ điều hành iOS có duy nhất một audio session, được truy cập trong code thông qua AVAudioSession.sharedIntance().

Audio session là trung gian giữa iOS và ứng dung, nó thể hiện các ý định tương tác với audio mà người lập trình mong muốn. Ta thể hiện nó thông qua việc cấu hình các thuộc tính của Audio session.

Audio session có 3 tính năng cơ bản:

  • Thiết lập category: Một category là một key xác định một loại các audio behavior cho app của ta. Chi tiết về các loại category xem ở đây nhé.
  • Post notification về interruptions và route changes.
  • Trích xuất các thông số phần cứng.

Audio session là điều đầu tiên ta cần quan tâm và thiết lập bởi vì nó sẽ xác định các audio behavior cho ứng dụng.

AVAudioPlayer

Đơn giản như cái tên gọi của nó, đây là một audio player. Tuy nhiên nó cũng không hẳn mạnh mẽ như cái tên nó được hưởng, bởi vì ta chỉ có thể sử dụng lớp này để
mở các audio ở bộ nhớ trong hoặc từ buffer. Có nghĩa là bạn cần có file sẵn trong máy, hoặc down về xong xuôi rồi mới có thể play audio bằng AVAudioPlayer.

Về các tính năng hàn lâm, tham khảo docs của Apple nhé: AVAudioPlayer

Chú ý rằng, không như AVPlayer, AVAudioPlayer không tự động xử lý audio session interruption cũng như audio session route change, do đó ta cần chủ động xử lý. Tham khảo:

Responding to Audio Session Interruptions

Responding to Audio Session Route Changes

Thực ra AVAudioPlayer cũng có tự dừng khi gặp phải interruption, tuy nhiên khi việc ngắt play xảy ra với một cơ chế thiếu chủ động, nó tạo ra bug. Bản thân người viết đã làm việc với nó và phải đối phó với bug này. Chúng ta sẽ bàn tiếp về nó ở phần sau nhé!

AVPlayer

Có thể nói, đây là một loại player hết sức là ghê gớm. Tại sao người ta chỉ gọi nó là Player mà không phải kiểu như AudioPlayer?

Bởi vì nó play cả video được luôn thưa quý bạn!

Điều đó có nghĩa là gì, có nghĩa là đây là một chiếc player đa di năng, nó có thể play audio/video từ local, audio/video từ kết nối mạng bên ngoài. Ngoài ra, nó cũng cung cấp cơ chế play khác với AVAudioPlayer, đó là thông qua AVPlayerItem. Nhờ đó mà ta có thể tách rời logic xử lý lỗi giữa player và item, cover được một số case phức tạp hơn

AVPlayer có khả năng tự động xử lý audio session interruption và route change. Nếu bạn muốn cập nhật lại UI dựa trên việc xử lý interrupt của AVPlayer, bạn chỉ cần observe một số thuộc tính cần thiết, ví dụ như timeControlStatus chẳng hạn.

Còn những thứ hàn lâm về AVPlayer, mời các bạn xem Apple docs nhé: AVPlayer

Và rồi cũng sẽ đến lúc bạn tự hỏi, AVPlayer bá đạo như thế thì ta dùng AVAudioPlayer để làm gì? AVPlayer vừa play được local file, vừa play được file từ server, sao không dùng 1 loại thôi?

Thực ra AVPlayerAVAudioPlayer có sự định hướng khác nhau trong mục đích sử dụng, do đó cấu trúc class của nó sẽ phản ánh điều này:

  • AVAudioPlayer đơn giản và thuận tiện hơn, nó có sẵn các thuộc tính tiện lợi như isPlaying, volume,… trong khi AVPlayer không có, tuy rằng đều có thể thiết lập được nhưng một số chức năng cũng cần code khá dài dòng.
  • AVAudioPlayer là một phiên bản audio player tinh gọn và dễ dàng sử dụng hơn AVPlayer, và bởi chính vậy, nó cũng bị giới hạn khả năng sử dụng là chỉ với local file và buffer để có thể đảm bảo tính đơn giản của nó. Nếu bạn cung cấp tính năng streaming file từ kết nối trên mạng, có hàng đống case mà bạn phải xử lý, và khi đó bạn không thể đơn giản được nữa, bạn cần AVPlayer.
  • AVPlayer dường như là một phiên bản audio player phức tạp hơn, và tất cả các tính năng thêm thắt mà nó cung cấp là để thuận tiện nhất cho việc xây dựng và xử lý file streaming qua kết nối bên ngoài.

Và để trả lời cho câu hỏi ban đầu, xin thưa, bạn có thể bỏ hoàn toàn AVAudioPlayer để sử dụng duy nhất AVPlayer. Tuy nhiên, điều bạn vừa làm là trường hợp điển hình của câu nói "dùng dao mổ trâu giết gà". Sự cồng kềnh và nặng nề sẽ đặt lên những dòng code của bạn, và chuyên mục "thách bé maintain" sẽ bắt đầu sớm thôi.

AVAudioRecorder

Ghi âm là một công việc phức tạp, luôn luôn là vậy. Và vì AVAudioRecorder được xây dựng nhằm hướng tới việc giảm thiểu độ cồng kềnh của code khi thiết lập cho chức năng ghi âm, nó không cung cấp khả năng ghi đè, và để viết code cắt ghép với AVAudioRecorder thì đúng là thảm họa.

Rất tiếc phải nói rằng để cung cấp tính năng ghi âm một cách tử tế và hoàn thiện, phần lớn ta thường phải xuống tới tầng Core Audio, sử dụng Audio Queue Services hoặc Audio Units.

Về các tính năng cơ bản của AVAudioRecorder, tham khảo docs của Apple nhé: AVAudioRecorder

AVAudioEngine

Đây là cả một chân trời mới trong thế giới của AVFoundation. Được ra mắt từ iOS 8, AVAudioEngine là một thư viện Objective-C mô phỏng cơ chế của Audio Unit trong Core Audio.

Nếu bạn chưa biết cơ chế của Audio Unit thì có thể hiểu AVAudioEngine giống như là một phần mềm remix. Chúng ta có thể kết nối nhiều node vào với nhau, trộn âm thanh nhờ một hay nhiều player node với các effect node phong phú và cung cấp sẵn từ bộ thư viện của AVAudioEngine, sau đó xuất ra một bản remix tuyệt vời.

Chúng ta cũng có thể ghi âm với khả năng xử lý cắt ghép tốt do ghi âm với AVAudioEngine không hoàn toàn đóng kín như AVAudioRecorder.

Tuy nhiên, đây là một thư viện mới và theo kinh nghiệm sử dụng nó của tôi thì cái thư viện này thực sự đầy bug, nhiều khi nói thật là crash như chơi. Apple docs không có nhiều hướng dẫn chi tiết về việc thiết lập sao cho nó "best practice", chỉ có một số đóng góp từ cộng đồng và một số tutorial.

Nếu bạn cần nhiều effect hơn là việc chỉ thay đổi play speed? Có lẽ bạn cần chuyển từ AVAudioPlayer sang AVAudioEngine.
Nếu bạn đang muốn ghi âm với AVAudioEngine? Vừa nên lại vừa không. Còn làm thế nào thì chúng ta sẽ sang bài sau nhé!

Kết

Phần 1 sẽ kết thúc ở đây, bài tiếp theo tôi sẽ hướng dẫn triển khai đồng thời đưa ra một số lời khuyên từ bản thân qua kinh nghiệm thu được khi làm việc với audio nhé mọi người!