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 o…


This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Raymond Yan

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"
)

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)
    }
}

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"

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"`
}

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"`
}

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"
)

We also need to define 3 package-level variables:

var conn *websocket.Conn
var done = false
var ticker *time.Ticker

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

func Start(url string) { 

}

(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

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)
        }
}()

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)
    }
}

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()
    }()
}

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
}

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"
)

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

var stream beep.StreamSeekCloser

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)
}

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

func Stop() {
    stream.Close()
}

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"
)

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"

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]
    }
}

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)
}

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()

(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


This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Raymond Yan


Print Share Comment Cite Upload Translate Updates
APA

Raymond Yan | Sciencx (2022-10-27T18:44:42+00:00) Build A K-pop Radio in Go!. Retrieved from https://www.scien.cx/2022/10/27/build-a-k-pop-radio-in-go/

MLA
" » Build A K-pop Radio in Go!." Raymond Yan | Sciencx - Thursday October 27, 2022, https://www.scien.cx/2022/10/27/build-a-k-pop-radio-in-go/
HARVARD
Raymond Yan | Sciencx Thursday October 27, 2022 » Build A K-pop Radio in Go!., viewed ,<https://www.scien.cx/2022/10/27/build-a-k-pop-radio-in-go/>
VANCOUVER
Raymond Yan | Sciencx - » Build A K-pop Radio in Go!. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/10/27/build-a-k-pop-radio-in-go/
CHICAGO
" » Build A K-pop Radio in Go!." Raymond Yan | Sciencx - Accessed . https://www.scien.cx/2022/10/27/build-a-k-pop-radio-in-go/
IEEE
" » Build A K-pop Radio in Go!." Raymond Yan | Sciencx [Online]. Available: https://www.scien.cx/2022/10/27/build-a-k-pop-radio-in-go/. [Accessed: ]
rf:citation
» Build A K-pop Radio in Go! | Raymond Yan | Sciencx | https://www.scien.cx/2022/10/27/build-a-k-pop-radio-in-go/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.