Экспериментируя с ShazamKit — ЗаShazamим Всё!

Копошилка

Экспериментируя с ShazamKit — ЗаShazamим Всё!

На WWDC всегда есть отличная коллекция музыки для отличного настроения. Во время WWDC’19 мой друг, используя свой телефон на Android, распознавал игравшие песни и создал из них плэйлист. У моего iPhone тогда такой возможности не было (или я о такой не знал). Теперь с помощью ShazamKit давайте создадим для этой цели приложение и назовем его Musadora!

В этой статье мы последовательно пройдемся через следующее:

  1. Что такое ShazamKit ?
  2. Как это работает
  3. Службы приложения ShazamKit
  4. Распознавание Музыки
  5. Библиотека Shazam
  6. Создание пользовательского интерфейса с помощью SwiftUI
  7. Распознавание фона

Внимание - это бета-версия программы, которая в будущем в любое время может измениться.
Для этого туториала вам понадобится Xcode 13.0 и iOS 15.0. При написании этого поста я использовал Xcode 13.0 Beta 2 и iOS 15.0 Beta 2.

Что такое ShazamKit?

ShazamKit - это фреймворк от Apple, который помогает вам как разработчику встроить распознавание музыки в ваше приложение. Это может быть или распознавание песен из каталога Shazam, или даже ваше собственное аудио. Вы можете добавить определившуюся песню в историю распознавания музыки Shazam. Apple неожиданно сделала его доступным и для Android.

Как это работает

Раньше я предполагал, что Shazam действительно сопоставляет фактическое аудио песни с их обширной библиотекой. Но, согласно документации,

чтобы найти пару, ShazamKit использует уникальную акустическую подпись аудиозаписи. Данная подпись фиксирует частотно-временное распределение энергии аудиосигнала, и она значительно меньше оригинального звука. Это также одностороннее преобразование, поэтому изменение подписи обратно в запись маловероятно.

Проще говоря, Shazam создает частотный график на основе времени под названием Spectrogram для каждого аудио в каталоге. Далее на основе графика создается подпись. Референциальная (ссылочная) подпись - это уникальная подпись и метаданные с информацией о песне.

Музыкальный каталог Shazam - это коллекция подобных уникальных ссылочных подписей. ShazamKit создает аудиообраз отправляемой ему записи, чтобы в дальнейшем сравнить её с каталогом. Если имеется достаточная доля соответствия, он возвращает нам такую информацию, как название песни и её исполнитель. Его алгоритм вполне мощный, чтобы распознавать музыку даже на шумном фоне.

Т.к. аудиообраз не может быть преобразован обратно в исходную запись, содержимое остается безопасным и приватным.
Существует целая научно-исследовательская работа по их алгоритму для тех, кому любопытно узнать больше.

Начало работы

Чтобы начать работу с ShazamKit и его сервисами, мы должны включить его для идентификатора нашего приложения. На портале для разработчиков, в разделе «Certificates, Identifiers, и Profiles» перейдите на вкладку «Identifiers» на боковой панели и нажмите значок «Add», чтобы создать новый идентификатор приложения.

Нажмите Продолжить. Назовите Bundle ID в соответствии с вашими предпочтениями. В разделе «App Services» выберите ShazamKit для использования его функционала.

ShazamKit

Он разделен на три компонента:

  • Распознавание каталога Shazam
  • Распознавание каталога Пользователя
  • Управление Библиотекой

Мы будем использовать распознавание каталога Shazam для определения песен в нашем приложении. Затем мы добавим их в библиотеку Shazam.

Распознавание Музыки

Откройте Xcode и создайте новый iOS проект на SwiftUI с названием Musadora. Создайте новый файл Swift с именем HomeViewModel.swift и добавьте в него следующий класс:

import Combine
import ShazamKit
import AVKit

@MainActor
class HomeViewModel: NSObject, ObservableObject {
  // 1
  @Published private(set) var mediaItems: [SHMediaItem] = []
  // 2
  @Published private(set) var isRecognizingSong = false

  // 3
  private let session = SHSession()

  // 4
  private let audioEngine = AVAudioEngine()

  private let feedback = UINotificationFeedbackGenerator()

  override init() {
    super.init()
    // 5
    session.delegate = self
  }
}

Здесь разбивка данного кода:

  1. Массив SHMediaItem представляет собой метаданные для ссылочной подписи. Каждый раз, когда Shazam находит совпадение, мы добавляем элемент в этот массив.
  2. @Published переменная isRecognizingSong помогает нам управлять состоянием распознавания песни.
  3. Объект session используется для управления сопоставлением аудиозаписи.
  4. AVAudioEngine одновременно преобразует звук в подпись во время записи.
  5. Результаты SHSession передаются через его делегата. Присвоив session.delegate значение self, мы можем получить доступ ко всем его методам.

Сначала мы начинаем с кода для записи аудио. Добавьте следующее в HomeViewModel.swift:

// MARK: Audio Recognition
extension HomeViewModel {
  // 1
  private func prepareAudioRecording() throws {
    let audioSession = AVAudioSession.sharedInstance()

    try audioSession.setCategory(.record)
    try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
  }

  // 2
  private func generateSignature() {
    let inputNode = engine.inputNode
    let recordingFormat = inputNode.outputFormat(forBus: .zero)

    inputNode.installTap(onBus: .zero, bufferSize: 1024,
                         format: recordingFormat) { [weak session] buffer, _ in
      session?.matchStreamingBuffer(buffer, at: nil)
    }
  }

  // 3
  private func startAudioRecording() throws {
    try engine.start()

    isRecognizingSong = true
  }
}

Суть приложения в Распознавании. Поэтому вот подробное пояснение:

  1. Начинаем с общего экземпляра AVAudioSession и настраиваем его для записи звука с микрофона. Далее активируем параметр audioSession.
  2. Создаем переменную inputNode для хранения текущей дорожки входного аудиосигнала. Мы создаем audioFormat со стандартным форматом. Настраиваем входной сигнал микрофона на поддерживаемый аудиоформат. Далее мы устанавливаем аудиоразъем (отвод) на шину. В данном блоке, находящееся в буфере аудио мы конвертируем в подпись и ищем ссылочные подписи в каталоге сессии.
  3. Запускаем аудио движок и устанавливаем isRecognizingSong в значение true.

Для упрощения процесса распознавания аудио, мы создаем два публичных метода для вызова в наших View:

public func startRecognition() {
  feedback.prepare()

  // 1
  do {
    if engine.isRunning {
      stopRecognition()
      return
    }

    // 2
    try prepareAudioRecording()

    generateSignature()

    try startAudioRecording()

    feedback.notificationOccurred(.success)
  } catch {
    // Handle errors here
    print(error)
    feedback.notificationOccurred(.error)
  }
}

public func stopRecognition() {
  isRecognizingSong = false
  engine.stop()
  engine.inputNode.removeTap(onBus: .zero)
}

Пройдемся по коду:

  1. Если аудио движок уже запущен, то срабатывает функция остановки распознавания: останавливается движок и убирается разъем с входного узла.
  2. Подготавливает аудио к записи, генерирует подпись и начинает аудиозапись.

SHSessionDelegate предоставляет нам два метода: один при обнаружении совпадения, а другой - при отсутствии совпадения. Когда мы находим совпадение, он возвращает объект SHMatch, представляющий медиа элементы каталога. Мы получаем доступ к первому элементу в элементах. Если mediaItems уже содержит данную песню, мы ее игнорируем. Если распознанная песня новая, мы добавляем ее в массив.

// MARK:- SHSessionDelegate
extension HomeViewModel: SHSessionDelegate {
  func session(_ session: SHSession, didFind match: SHMatch) {

    guard let mediaItem = match.mediaItems.first else { return }

    async {
      if mediaItems.contains(where: { $0.shazamID == mediaItem.shazamID }) {
        // Song already identified and in the list. Do nothing.
      } else {
        self.mediaItems.append(mediaItem)
      }
    }
  }
}

Добавление музыки в Библиотеку Shazam

Вы также можете добавить распознанную музыку из вашего приложения в библиотеку Shazam. Для этого добавьте следующий код в HomeViewModel:

public func addToShazamLibrary() {
  SHMediaLibrary.default.add(mediaItems) { error in
    if let error = error {
      print(error)
    } else {
      let generator = UINotificationFeedbackGenerator()
      generator.notificationOccurred(.success)
    }
  }
}

Мы получаем доступ к пользовательской библиотеке Shazam по умолчанию и добавляем в нее список mediaItems.

Заметка

Начиная с 3 беты, медиа-элементы, добавленные в дефолтный экземпляр SHMediaLibrary, не отображаются в Shazam. (77785557)
Обходной путь: Нажмите и удерживайте модуль Центра управления распознаванием музыки для просмотра содержимого SHMediaLibrary.

Благодаря этому мы написали функционал, запрашиваемый для распознавания музыки с ShazamKit. Давайте создадим UI, чтобы применить его!

Создание Пользовательского Интерфейса с использованием SwiftUI

Вы можете спроектировать экран исходя из своих предпочтений. Я использую обложку альбома и отображаю название песни и имя исполнителя рядом с ней.

struct ShazamMusicCard: View {
  var item: SHMediaItem

  var body: some View {
    HStack {
      ArtworkImage(url: url) { image in
        image
          .scaledToFit()
          .transition(.opacity.combined(with: .scale))
      }
      .cornerRadius(12.0)
      .frame(width: 100.0, height: 100.0)

      VStack(alignment: .leading, spacing: 4.0) {
        Text(name)
          .fontWeight(.bold)
          .font(.callout)

        Text(artistName)
          .fontWeight(.light)
          .font(.caption)
      }
      .foregroundColor(.white)
      .frame(maxWidth: .infinity, alignment: .leading)
      .multilineTextAlignment(.leading)
    }
  }

  private var name: String {
    item.title ?? ""
  }

  private var artistName: String {
    item.artist ?? ""
  }

  private var url: URL? {
    item.artworkURL
  }
}

Добавляем карточку поверх размытого фонового изображения:

struct ShazamMusicRow: View {
  var item: SHMediaItem

  var body: some View {
    ZStack {
      ArtworkImage(url: item.artworkURL) { image in
        image
          .scaledToFill()
          .layoutPriority(-1)
          .overlay(Color.black.opacity(0.8))
          .transition(.opacity.combined(with: .scale))
      }

      ShazamMusicCard(item: item)
        .background(.ultraThinMaterial)
    }
    .cornerRadius(12.0)
    .padding(4.0)
  }
}

ArtworkImage - удобное view для использования последней версии AsyncImage в SwiftUI Release 3.0 и некоторых view по умолчанию для обработки различных этапов:

struct ArtworkImage: View where Content: View {
  private let url: URL?
  private var content: (_ image: Image) -> Content

  public init(url: URL?, @ViewBuilder content: @escaping (_ image: Image) -> Content) {
    self.url = url
    self.content = content
  }

  var body: some View {
    if let url = url {
      AsyncImage(url: url, transaction: .init(animation: .spring())) { phase in
        switch phase {
        case .empty: progressView()
        case .success(let image): content(image.resizable())
        case .failure(let error as NSError): errorView(with: error)
        @unknown default: unknownView()
        }
      }
    } else {
      Text("Wrong URL")
    }
  }

  private func progressView() -> some View {
    ProgressView().transition(.opacity.combined(with: .scale))
  }

  private func errorView(with error: NSError) -> some View {
    ZStack {
      Color.red.transition(.opacity.combined(with: .scale))

      Text(error.localizedDescription).foregroundColor(.white)
    }
    .transition(.opacity.combined(with: .scale))
  }

  private func unknownView() -> some View {
    Color.gray.transition(.opacity.combined(with: .scale))
  }
}

Создаем другой SwiftUI файл с названием HomeButtonsView.swift и добавляем в него:

extension Image {
  func imageButton(with size: CGFloat, color: Color) -> some View {
    self
      .resizable()
      .scaledToFit()
      .frame(width: size, height: size)
      .foregroundColor(color)
  }
}

struct ButtonImageType {
    static let addToLibrary = "square.and.arrow.down.fill"
    static let startRecognition = "shazamIcon"
    static let stopRecognition = "stop.circle.fill"
}

struct HomeButtonsView: View {
  @ObservedObject var viewModel: HomeViewModel

  private let size = 50.0

  private var isRecognizingSong: Bool {
    viewModel.isRecognizingSong
  }

  var body: some View {
    HStack {
      // 1
      Button(action: { viewModel.addToShazamLibrary() }) {
          Image(systemName: ButtonImageType.addToLibrary)
          .imageButton(with: size, color: .green)
      }

      Spacer()

      // 2      
      Button(action: { viewModel.startRecognition() }) {
          Image(ButtonImageType.startRecognition)
          .imageButton(with: size * 2, color: .clear)
      }
      .disabled(isRecognizingSong)
      .scaleEffect(isRecognizingSong ? 0.8 : 1)
      .animation(songRecognitionAnimation(), value: isRecognizingSong)

      Spacer()

      // 3
      Button(action: { viewModel.stopRecognition() }) {
          Image(systemName: ButtonImageType.stopRecognition)
          .imageButton(with: size, color: .red)
      }
    }
    .padding(.horizontal, 24)
  }

  // 4
  private func songRecognitionAnimation() -> Animation {
    isRecognizingSong ? .easeInOut(duration: 1.5).repeatForever() : .default
  }
}

Что делает данный код:

  1. Добавляем список медиа элементов в библиотеку Shazam. Если вы откроете центр управления и долго нажмете на иконку Shazam, вы найдете распознанные песни с упоминанием названия приложения.
  2. Запускаем распознавание аудио нажатием на кнопку Shazam.
  3. Останавливаем распознавание аудио нажатием на кнопку Stop.
  4. Анимируем кнопку с помощью простой анимации движения, пока она распознает песни.

Завершая процесс проектирования, мы добавляем ShazamMusicRow в список и HomeButtonsView под ним.

struct ContentView: View {
  @StateObject private var viewModel = HomeViewModel()
  @Environment(\.openURL) var openURL

  var body: some View {
    NavigationView {
      VStack {
        List {
          ForEach(viewModel.mediaItems, id: \.shazamID) { item in
            Button(action: { openAppleMusic(with: item.appleMusicURL) }) {
              ShazamMusicRow(item: item)
            }
            .buttonStyle(.plain)
            .listRowSeparator(.hidden)
          }
        }
        .listStyle(.plain)

        HomeButtonsView(viewModel: viewModel)
      }
      .navigationTitle("Musadora")
    }
    .navigationViewStyle(.stack)
  }

  private func openAppleMusic(with url: URL?) {
    if let url = url {
      openURL(url)
    }
  }
}

Мы перебираем медиа-элементы, и при нажатии на отдельную строку открываем эту конкретную песню в Apple Music.
Теперь мы готовы Shazamить всё! Запустите приложение и наслаждайтесь своим собственным приложением с плейлистом Shazam!

Распознавание фона

В данный момент приложение распознает музыку только когда она играет на переднем плане. Представьте, что вы находитесь на вечеринке и хотите создать плэйлист из классных песен. Сделав наше приложение более эффективным, мы включим фоновый режим, чтобы вы могли держать телефон в кармане и наслаждаться вечеринкой!

Выберите в Targets Musadora, выберите Signing & Capabilities и нажмите кнопку Capability. Найдите Background Modes (фоновые режимы) и добавьте их. В разделе Background Modes установите флажок напротив Audio, Airplay, and Picture in Picture. Это поможет нам записывать и распознавать аудио в фоновом режиме.

Запустите приложение, нажмите кнопку Shazam и заблокируйте iPhone. Воспроизведите музыку и перепроверьте приложение. Вы найдете распознанную песню в списке!

Вывод

Shazam - это привлекательное приложение для почти мгновенного распознавания музыки. Благодаря Apple, дающей нам возможность экспериментировать, мы можем создавать творческие приложения или интегрировать их в существующие!

Надеюсь, вам понравился этот туториал. Давайте всё заShazamим!

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: