Comment construire un serveur de streaming audio en Go

Regardez ce petit Gopher musical!

L'audio dans le navigateur n'est pas toujours bien accueilli à bras ouverts. La plupart des vidéos partagées sur les réseaux sociaux anticipent désormais que les personnes les regardent avec le son désactivé, les pages d'accueil avec la musique de fond en lecture automatique (RIP Geocities) sont depuis longtemps oubliées, et les boutons sur le Web qui font en sorte que le clic sonore soit une chose qui n'a jamais décollé. , le web est plutôt silencieux.

Toutefois, le streaming audio, à la demande ou en direct, n’a jamais été aussi populaire; Des services tels que Spotify, Twitch et des logiciels d’appel vidéo tels que Skype ou Discord transforment tous les sons en sons que nous faisons en 1 et en 0 et les envoient aux auditeurs du monde entier via le Web. Étonnamment, peu d’informations sur la façon de procéder sont disponibles en ligne, du moins non destinées aux débutants.

Et voilà: “Comment construire un serveur de streaming audio”, visait à amener des gens comme moi qui ont des connaissances en audio et / ou en musique et une compréhension de base du fonctionnement du Web passionnés par le streaming et l'audio numérique en général.

Ici, nous allons nous concentrer sur la construction d’un serveur très simple qui achemine l’audio d’un endroit à l’autre, via la magie de http. Ce serveur obtiendra une entrée audio à partir de n’importe quelle entrée audio disponible sur le serveur, telle que le microphone de votre Macbook si vous exécutez le serveur sur un Macbook, le convertissez en binaire et envoyez une réponse groupée au client. Couper la réponse dans http est un moyen d'envoyer des données partielles, ou «morceaux» de données, ce qui est particulièrement utile si vous avez un jeu de données qui n'est pas encore complet. Pensez-y: lorsque vous enregistrez, vous n’avez pas encore d’enregistrement complet, car vous enregistrez encore. Chaque fois que vous arrêtez d’enregistrer, c’est lorsque vous avez un enregistrement complet sous la forme d’un fichier .wav ou .mp3. Nous sommes intéressés par l’envoi des données avant la fin de l’enregistrement, c’est-à-dire avant que le fichier ‘recording.wav’ ne soit terminé.

Nous allons utiliser Go pour ce petit projet, car je trouve que c’est l’idéal pour un petit serveur http simple qui n’a pas besoin de beaucoup d’installation pour fonctionner. Nous utiliserons également une bibliothèque appelée Portaudio qui gérera toutes les E / S audio. C’est pratique car il gère l’audio quel que soit le système d’exploitation du serveur (OSX / Windows / Linux). Portaudio fonctionne avec une boucle audio principale dans laquelle vous ferez quelque chose avec l'entrée et la sortie audio. Dans ce cas, nous ne voulons que l’entrée - la sortie serait celle des haut-parleurs du serveur (ou de la sortie audio principale configurée sur le serveur) dont nous n’avions pas besoin. Nous sommes intéressés par l'enregistrement de l'entrée audio dans un tampon, que nous pouvons ensuite extraire via http. Notre boucle principale (et la configuration de Portaudio) se présente comme suit:

paquet principal
importation (
 "encodage / binaire"
 "github.com/gordonklaus/portaudio"
 "net / http"
)
const sampleRate = 44100
const secondes = 1
func main () {
 portaudio.Initialize ()
 reporter portaudio.Terminate ()
 buffer: = make ([] float32, sampleRate * seconds)
 stream, err: = portaudio.OpenDefaultStream (1, 0, sampleRate, len (tampon), func (dans [] float32) {
  pour i: = range buffer {
   tampon [i] = dans [i]
  }
 })
 si err! = nil {
   panique (err)
 }
 stream.Start ()
 reporter le flux.Fermer ()
}

Nous commençons par initialiser () portaudio et deferTerminate (). Nous allons ensuite créer une tranche de mémoire tampon et la définir à la même longueur que notre fréquence d'échantillonnage. La fréquence d'échantillonnage déterminera le nombre d'échantillons que contient une seconde d'audio. Le réglage de la mémoire tampon sur la même fréquence d'échantillonnage signifie qu'il contiendra une seconde d'audio. Nous configurons ensuite un flux par défaut (c’est-à-dire qu’il utilisera l’entrée et la sortie par défaut ou, dans ce cas, uniquement l’entrée) avec OpenDefaultStream (), avec une entrée, zéro sortie, un taux d’échantillonnage de 44100 et nous spécifions nos images par image. buffer doit être la longueur de notre tampon précédemment créé. Ensuite, nous passons dans une fonction qui a notre entrée comme argument, qui sera la boucle audio principale. Ici, nous mappons simplement les valeurs contenues dans le tableau in à notre tableau tampon. Enfin, nous commençons le flux et différons stream.Close () lorsque vous avez terminé.

Ensuite, nous pouvons créer une route / audio simple qui enverra le tampon sous forme de réponse fragmentée. Nous créons un http.Flusher utilisé par Go pour envoyer des morceaux de données et définissons l'en-tête Transfer-Encoding sur "chunked". Nous créons une boucle infinie dans laquelle nous encodons le tampon en binaire et l'écrivons dans le flux.

http.HandleFunc ("/ audio", func (w http.ResponseWriter, r * http.Request) {
  flusher, ok: = w. (http.Flusher)
  si ok
   panique ("http.ResponseWriter attendu comme un http.Flusher")
  }
  w.Header (). Set ("Connexion", "Keep-Alive")
  w.Header (). Set ("Transfer-Encoding", "chunked")
  Pour de vrai {
   binary.Write (w, binary.BigEndian, & buffer)
   flusher.Flush () // Déclenche l'encodage "chunked"
   revenir
  }
 })

Nous avons maintenant un programme de streaming audio très basique, qui capture l'audio à partir de l'entrée par défaut de votre ordinateur et le rend disponible au téléchargement via un encodage en bloc. Passons maintenant à la gestion de l’audio côté client!

Nous allons à nouveau utiliser Go et Portaudio pour créer un petit programme qui récupère les données binaires brutes de notre terminal / audio, les décode et les lit à l’aide de Portaudio. Il fonctionne à peu près comme notre programme précédent, à la différence qu’il écrit l’audio sur une sortie par défaut au lieu de lire à partir d’une entrée par défaut. Cela ressemble à ceci:

importation (
 "octets"
 "encodage / binaire"
 "fmt"
 "github.com/gordonklaus/portaudio"
 "io / ioutil"
 "net / http"
 "temps"
)
const sampleRate = 44100
const secondes = 1
func main () {
 portaudio.Initialize ()
 reporter portaudio.Terminate ()
 buffer: = make ([] float32, sampleRate * seconds)
stream, err: = portaudio.OpenDefaultStream (0, 1, sampleRate, len (tampon), func (out [] float32) {
  resp, err: = http.Get ("http: // localhost: 8080 / audio")
  chk (err)
  body, _: = ioutil.ReadAll (resp.Body)
  responseReader: = bytes.NewReader (body)
  binary.Read (responseReader, binary.BigEndian, & buffer)
  pour i: = va sur {
   out [i] = tampon [i]
  }
 })
 chk (err)
 chk (stream.Start ())
 time.Sleep (time.Second * 40)
 chk (stream.Stop ())
 reporter le flux.Fermer ()
}
func chk (err error) {
 si err! = nil {
  panique (err)
 }
}

Nous utilisons la méthode ReadAll du paquet ioutil pour lire le corps de la réponse http que nous obtenons de notre serveur. Pour lire le corps, nous avons également besoin d'un lecteur de réponse, que nous pouvons obtenir à partir du paquet d'octets via NewReader (). Nous utilisons ensuite la méthode Read () du paquet binaire, qui fait l’opposée de la méthode Write () que nous avons utilisée sur notre serveur. Enfin, nous mappons le contenu de notre tampon de lecture sur la sortie, que Portaudio lira comme un son magnifique et vierge.

Évidemment, cet exemple est un peu défectueux - il obtiendra uniquement la réponse suivante du noeud final / audio à la fin de la boucle ou à la fin de la lecture de la mémoire tampon. Une excellente étape suivante serait d’obtenir les données de réponse de manière asynchrone, en utilisant éventuellement une routine ou un canal Go. Peut-être que les tampons doivent être stockés dans un tableau de tampons, afin que nous puissions les lire les uns après les autres! Si vous avez des idées à ce sujet, n’ayez pas peur de laisser un commentaire.

Vous pouvez trouver le code d'exemple complet ici!

Edit: Au départ, j’ai voulu faire une série en plusieurs parties, mais j’ai finalement décidé de ne pas en faire, principalement à cause de contraintes de temps. Merci d'avoir lu!