DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Build A K-pop Radio in Go!
Raymond Yan
Raymond Yan

Posted on

Build A K-pop Radio in Go!

The full code can be found here:
https://github.com/raymond-design/kpop-cli

Intro:

We'll be learning how to use the Gorilla websockets websocket client and faiface/beep to stream K-pop music 🎹

Setup the project:

Let's first init our Go project by running:
go mod init [project name]

We'll need to download two libraries:

Beep:
go get github.com/faiface/beep

Gorilla Websocket:
go get github.com/gorilla/websocket

Start coding!

It will be helpful if we first setup our project structure.

First create a main.go file at in our project directory. This will be our entrypoint.

Then create 4 more directories:
connect
play
types
ui

The project structure should look something like this (I also recommend creating a .gitignore if you plan on pushing to git:

The Project opened up in VsCode

User Interface

Let's first create a file inside the ui folder. We name the file ui.go.

This file will define a function that prints song info the terminal! First let's import the "fmt" package:

package ui

import (
    "fmt"
)
Enter fullscreen mode Exit fullscreen mode

Now let's create a function named WriteToFunction. Make sure to capitalize the first letter (since we'll use it elsewhere):

func WriteToScreen(name string, author string, album string) {
    fmt.Print("\033[H\033[2J")
    fmt.Println("Now Playing:")
    fmt.Println("Title: " + name)
    fmt.Println("Artist: " + author)
    if album != "" {
        fmt.Println("Album: " + album)
    }
}
Enter fullscreen mode Exit fullscreen mode

ui.go looks like this:

ui.go

Define Types

A helpful pattern in Go is to define related struct types in one place. Let's create a types.go file in the types directory.

The song info will be in json format. First import that:

package types

import "encoding/json"
Enter fullscreen mode Exit fullscreen mode

Next, we need to describe some types for WebSockets connection:

type SocketRes struct {
    Op int64 `json:"op"`
    D  json.RawMessage
}

type SendData struct {
    Op int64 `json:"op"`
}

type HeartbeatData struct {
    Message   string `json:"message"`
    Heartbeat int64  `json:"heartbeat"`
}
Enter fullscreen mode Exit fullscreen mode

Next, we will define some structs related to the songs themselves(Song, Album, etc.):

type PlayingData struct {
    Song       Song        `json:"song"`
    Requester  interface{} `json:"requester"`
    Event      interface{} `json:"event"`
    StartTime  string      `json:"startTime"`
    LastPlayed []Song      `json:"lastPlayed"`
    Listeners  int64       `json:"listeners"`
}

type Song struct {
    ID       int64         `json:"id"`
    Title    string        `json:"title"`
    Sources  []interface{} `json:"sources"`
    Artists  []Album       `json:"artists"`
    Albums   []Album       `json:"albums"`
    Duration int64         `json:"duration"`
}

type Album struct {
    ID         int64   `json:"id"`
    Name       string  `json:"name"`
    NameRomaji *string `json:"nameRomaji"`
    Image      *string `json:"image"`
}
Enter fullscreen mode Exit fullscreen mode

Now that we finished definitions, we can create the client to stream audio!

Create the WebSocket Client πŸ”Œ

Head over to the connect directory and create a connect.go file.

In this package, we'll need to import Gorilla websocket and the two packages we've already created:

package connect

import (
    "encoding/json"
    "log"
    "time"

    "github.com/raymond-design/kpop-cli/types"
    "github.com/raymond-design/kpop-cli/ui"

    "github.com/gorilla/websocket"
)
Enter fullscreen mode Exit fullscreen mode

We also need to define 3 package-level variables:

var conn *websocket.Conn
var done = false
var ticker *time.Ticker
Enter fullscreen mode Exit fullscreen mode

Let's a create a function to initialize the connection:

func Start(url string) { 

}
Enter fullscreen mode Exit fullscreen mode

(Later on, url string will be the WebSocket server url that we want to stream from)

Now paste the following:

conn_l, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
    log.Fatal("Couldn't connect to websocket")
}
conn = conn_l
Enter fullscreen mode Exit fullscreen mode

If the conn doesn't work, there could be an error with the URL!

Now, let's run an anonymous function Goroutine to maintain the WebSocket connection:

go func() {
        for {
            if done {
                conn.Close()
                break
            }
            _, msg, err := conn.ReadMessage()
            if err != nil {
                log.Fatal("Couldn't read websocket message")
            }

            handleMessage(msg)
        }
}()
Enter fullscreen mode Exit fullscreen mode

We will keep on maintaining the connection until program break or a read error. The function should look something like this:

function code

Now we need to implement that handleMessage function!

func handleMessage(in []byte) {
    var msg types.SocketRes
    json.Unmarshal(in, &msg)
    switch msg.Op {
    case 0:
        var data types.HeartbeatData
        json.Unmarshal(msg.D, &data)
        setHeartbeat(data.Heartbeat)
    case 1:
        var data types.PlayingData
        json.Unmarshal(msg.D, &data)
        album := "None"
        if len(data.Song.Albums) > 0 {
            album = data.Song.Albums[0].Name
        }
        ui.WriteToScreen(data.Song.Title, data.Song.Artists[0].Name, album)
    }
}
Enter fullscreen mode Exit fullscreen mode

In the start function, we continually call this function which will grab the current song data and print it.

To make the code cleaner, the actual set heartbeat logic will be in two other functions:

func sendHeartBeat() {
    data := types.SendData{
        Op: 9,
    }
    conn.WriteJSON(data)
}

func setHeartbeat(repeat int64) {
    sendHeartBeat()
    ticker = time.NewTicker(time.Duration(repeat) * time.Millisecond)
    go func() {
        <-ticker.C
        sendHeartBeat()
    }()
}
Enter fullscreen mode Exit fullscreen mode

If you want to read more about WebSockets connections, here's a helpful article:
https://www.programmerall.com/article/821816187/

Finally, we just need a stopping function that will break out of that for loop:

func Stop() {
    ticker.Stop()
    done = true
}
Enter fullscreen mode Exit fullscreen mode

Now that we have these WebSockets connection functions, we can bring sound into the app!

Including Sound!

To bring in sound, we will be importing faiface/beep:

package play

import (
    "log"
    "net/http"
    "time"

    "github.com/faiface/beep"
    "github.com/faiface/beep/mp3"
    "github.com/faiface/beep/speaker"
)
Enter fullscreen mode Exit fullscreen mode

We will also create a global var from this beep package:

var stream beep.StreamSeekCloser
Enter fullscreen mode Exit fullscreen mode

We will need two functions. One to play and one to stop.

The play function is quite simple. We will check the validity of the http url and then use the beep/mp3 to starting streaming audio contents!

func Play(url string) {
    resp, err := http.Get(url)
    if err != nil {
        log.Fatal("http error")
    }

    l_streamer, format, err := mp3.Decode(resp.Body)
    stream = l_streamer
    if err != nil {
        log.Fatal("decoding error")
    }

    speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
    speaker.Play(stream)
}
Enter fullscreen mode Exit fullscreen mode

The stop function is even simpler. We just close the stream:

func Stop() {
    stream.Close()
}
Enter fullscreen mode Exit fullscreen mode

The code looks something like this:

Play Audio Code

Project Entrypoint

Now we can create the entrypoint to our app! Let's import our packages:

package main

import (
    "fmt"
    "os"
    "os/signal"

    "github.com/raymond-design/kpop-cli/connect"
    "github.com/raymond-design/kpop-cli/play"
)
Enter fullscreen mode Exit fullscreen mode

Now let's define the server URL that we'll stream from:

const JPOP string = "https://listen.moe/fallback"
const KPOP string = "https://listen.moe/kpop/fallback"

const JSOCKET string = "wss://listen.moe/gateway_v2"
const KSOCKET string = "wss://listen.moe/kpop/gateway_v2"
Enter fullscreen mode Exit fullscreen mode

By the way, you can also stream J-pop music now!

Now create the main function:

func main(){
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    mode := "kpop"
    var stream string
    var socket string

    if len(os.Args) == 2 {
        mode = os.Args[1]
    }
}
Enter fullscreen mode Exit fullscreen mode

We can use a switch function to switch between K-pop and J-pop music:

switch mode {
    case "kpop":
        stream = KPOP
        socket = KSOCKET
    case "jpop":
        stream = JPOP
        socket = JSOCKET
    default:
        fmt.Println("Error")
        os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

Now, we can connect and start streaming music!

connect.Start(socket)
play.Play(stream)

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
<-interrupt

fmt.Println("Exiting Player")
play.Stop()
connect.Stop()
Enter fullscreen mode Exit fullscreen mode

(Notice we stop first stop decoding audio, then disconnect from the WebSockets server)

The main function looks like this:

Main Function

Listening to the radio 🎢🀘

  • Run a go get to get all dependencies.
  • Run go build . in the project.
  • Run ./kpop-cli kpop to play K-pop music or ./kpop-cli jpop (If you implemented J-pop).

Now you know how to implement sound and WebSocket streaming in Go!

Also try streaming other types of data in the future πŸ˜€



The full code can be found here:
https://github.com/raymond-design/kpop-cli

Top comments (0)

Why You Need to Study Javascript Fundamentals

>> Check out this classic DEV post <<