[GO] I want to make a music player and file music at the same time

Last time, I was able to play music with Go https://qiita.com/usk81/items/8590172a23bb71e21329

I usually play Nico Douga by opening two browser tabs and playing the same song at the same time with different singers and doing a duet, so I thought I could do it with a program.

Original code

// package main plays two audio file
package main

import (
    "log"
    "os"
    "time"

    "github.com/faiface/beep"
    "github.com/faiface/beep/mp3"
    "github.com/faiface/beep/speaker"
)

func main() {
    f, err := os.Open("test.mp3")
    if err != nil {
        log.Fatal(err)
    }
    st, format, err := mp3.Decode(f)
    if err != nil {
        log.Fatal(err)
    }
    defer st.Close()

    speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))

    done := make(chan bool)
    speaker.Play(beep.Seq(st, beep.Callback(func() {
        done <- true
    })))
    <-done
}

Examine the speaker code

// Package speaker implements playback of beep.Streamer values through physical speakers.
package speaker

import (
	"sync"

	"github.com/faiface/beep"
	"github.com/hajimehoshi/oto"
	"github.com/pkg/errors"
)

var (
	mu      sync.Mutex
	mixer   beep.Mixer
	samples [][2]float64
	buf     []byte
	context *oto.Context
	player  *oto.Player
	done    chan struct{}
)

// Init initializes audio playback through speaker. Must be called before using this package.
//
// The bufferSize argument specifies the number of samples of the speaker's buffer. Bigger
// bufferSize means lower CPU usage and more reliable playback. Lower bufferSize means better
// responsiveness and less delay.
func Init(sampleRate beep.SampleRate, bufferSize int) error {
	mu.Lock()
	defer mu.Unlock()

	Close()

	mixer = beep.Mixer{}

	numBytes := bufferSize * 4
	samples = make([][2]float64, bufferSize)
	buf = make([]byte, numBytes)

	var err error
	context, err = oto.NewContext(int(sampleRate), 2, 2, numBytes)
	if err != nil {
		return errors.Wrap(err, "failed to initialize speaker")
	}
	player = context.NewPlayer()

	done = make(chan struct{})

	go func() {
		for {
			select {
			default:
				update()
			case <-done:
				return
			}
		}
	}()

	return nil
}

// Close closes the playback and the driver. In most cases, there is certainly no need to call Close
// even when the program doesn't play anymore, because in properly set systems, the default mixer
// handles multiple concurrent processes. It's only when the default device is not a virtual but hardware
// device, that you'll probably want to manually manage the device from your application.
func Close() {
	if player != nil {
		if done != nil {
			done <- struct{}{}
			done = nil
		}
		player.Close()
		context.Close()
		player = nil
	}
}

// Lock locks the speaker. While locked, speaker won't pull new data from the playing Stramers. Lock
// if you want to modify any currently playing Streamers to avoid race conditions.
//
// Always lock speaker for as little time as possible, to avoid playback glitches.
func Lock() {
	mu.Lock()
}

// Unlock unlocks the speaker. Call after modifying any currently playing Streamer.
func Unlock() {
	mu.Unlock()
}

// Play starts playing all provided Streamers through the speaker.
func Play(s ...beep.Streamer) {
	mu.Lock()
	mixer.Add(s...)
	mu.Unlock()
}

// Clear removes all currently playing Streamers from the speaker.
func Clear() {
	mu.Lock()
	mixer.Clear()
	mu.Unlock()
}

// update pulls new data from the playing Streamers and sends it to the speaker. Blocks until the
// data is sent and started playing.
func update() {
	mu.Lock()
	mixer.Stream(samples)
	mu.Unlock()

	for i := range samples {
		for c := range samples[i] {
			val := samples[i][c]
			if val < -1 {
				val = -1
			}
			if val > +1 {
				val = +1
			}
			valInt16 := int16(val * (1<<15 - 1))
			low := byte(valInt16)
			high := byte(valInt16 >> 8)
			buf[i*4+c*2+0] = low
			buf[i*4+c*2+1] = high
		}
	}

	player.Write(buf)
}

that?

var (
	mu      sync.Mutex
	mixer   beep.Mixer
	samples [][2]float64
	buf     []byte
	context *oto.Context
	player  *oto.Player
	done    chan struct{}
)

Isn't it just a matter of making a structure and making a method easy?

Try to make a modified version

package speaker

import (
	"log"
	"sync"

	"github.com/faiface/beep"
	"github.com/hajimehoshi/oto"
	"github.com/pkg/errors"
)

type Player struct {
	mu      sync.Mutex
	mixer   beep.Mixer
	samples [][2]float64
	buf     []byte
	context *oto.Context
	player  *oto.Player
	done    chan struct{}
}

// Init initializes audio playback through speaker. Must be called before using this package.
//
// The bufferSize argument specifies the number of samples of the speaker's buffer. Bigger
// bufferSize means lower CPU usage and more reliable playback. Lower bufferSize means better
// responsiveness and less delay.
func Init(sampleRate beep.SampleRate, bufferSize int) (p *Player, err error) {
	p = &Player{}
	p.mu.Lock()
	defer p.mu.Unlock()

	p.Close()

	p.mixer = beep.Mixer{}

	numBytes := bufferSize * 4
	p.samples = make([][2]float64, bufferSize)
	p.buf = make([]byte, numBytes)

	p.context, err = oto.NewContext(int(sampleRate), 2, 2, numBytes)
	if err != nil {
		return nil, errors.Wrap(err, "failed to initialize speaker")
	}
	log.Print("before NewPlayer")
	p.player = p.context.NewPlayer()
	log.Print("before NewPlayer")

	p.done = make(chan struct{})
	log.Print("make channel")

	go func() {
		for {
			select {
			default:
				p.update()
			case <-p.done:
				return
			}
		}
	}()

	return p, nil
}

// Close closes the playback and the driver. In most cases, there is certainly no need to call Close
// even when the program doesn't play anymore, because in properly set systems, the default mixer
// handles multiple concurrent processes. It's only when the default device is not a virtual but hardware
// device, that you'll probably want to manually manage the device from your application.
func (p *Player) Close() {
	if p.player != nil {
		if p.done != nil {
			p.done <- struct{}{}
			p.done = nil
		}
		p.player.Close()
		p.context.Close()
		p.player = nil
	}
}

// Lock locks the speaker. While locked, speaker won't pull new data from the playing Stramers. Lock
// if you want to modify any currently playing Streamers to avoid race conditions.
//
// Always lock speaker for as little time as possible, to avoid playback glitches.
func (p *Player) Lock() {
	p.mu.Lock()
}

// Unlock unlocks the speaker. Call after modifying any currently playing Streamer.
func (p *Player) Unlock() {
	p.mu.Unlock()
}

// Play starts playing all provided Streamers through the speaker.
func (p *Player) Play(s ...beep.Streamer) {
	p.mu.Lock()
	p.mixer.Add(s...)
	p.mu.Unlock()
}

// Clear removes all currently playing Streamers from the speaker.
func (p *Player) Clear() {
	p.mu.Lock()
	p.mixer.Clear()
	p.mu.Unlock()
}

// update pulls new data from the playing Streamers and sends it to the speaker. Blocks until the
// data is sent and started playing.
func (p *Player) update() {
	p.mu.Lock()
	p.mixer.Stream(p.samples)
	p.mu.Unlock()

	// buf := p.buf
	for i := range p.samples {
		for c := range p.samples[i] {
			val := p.samples[i][c]
			if val < -1 {
				val = -1
			}
			if val > +1 {
				val = +1
			}
			valInt16 := int16(val * (1<<15 - 1))
			low := byte(valInt16)
			high := byte(valInt16 >> 8)
			p.buf[i*4+c*2+0] = low
			p.buf[i*4+c*2+1] = high
		}
	}

	p.player.Write(p.buf)
}

I tried to implement

// package main plays two audio file
//   failure: oto.NewContext can be called only once
package main

import (
	"log"
	"os"
	"time"

	"github.com/faiface/beep"
	"github.com/faiface/beep/mp3"

	"github.com/usk81/til/go-duet-player/speaker"
)

func main() {
	f1, err := os.Open("test1.mp3")
	if err != nil {
		log.Fatal(err)
	}
	st1, format, err := mp3.Decode(f1)
	if err != nil {
		log.Fatal(err)
	}
	defer st1.Close()

	sp1, err := speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
	if err != nil {
		log.Fatal(err)
	}

	done1 := make(chan bool)

	f2, err := os.Open("test2.mp3")
	if err != nil {
		log.Fatal(err)
	}
	st2, format, err := mp3.Decode(f2)
	if err != nil {
		log.Fatal(err)
	}
	defer st2.Close()

	sp2, err := speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
	if err != nil {
		log.Fatal(err)
	}

	done2 := make(chan bool)

	sp1.Play(beep.Seq(st1, beep.Callback(func() {
		done1 <- true
	})))
	sp2.Play(beep.Seq(st2, beep.Callback(func() {
		done2 <- true
	})))
	<-done1
	<-done2
}

Yeah, it doesn't work

oto: NewContext can be called only once

I couldn't because the Context of ʻoto` on which beep depends doesn't seem to support concurrency. This time I'm making something every day, so I'll give up on that layer.

I tried it with Python for the time being.

from pydub import AudioSegment
from pydub.playback import play

#Load an audio file
audio1 = "test1.mp3"
audio2 = "test2.mp3"

sound1 = AudioSegment.from_mp3(audio1)
sound2 = AudioSegment.from_mp3(audio2)

combined = sound1.overlay(sound2)
play(combined)

It moved! !!

Somehow regrettable ... (`; ω; ´)

Remarks

It is not possible to adjust the position at the beginning of singing, so please adjust it by playing with music editing software.

Recommended Posts

I want to make a music player and file music at the same time
I want to extract the tag information (title and artist) of a music file (flac, wav).
I want to create a Dockerfile for the time being.
I want to get information from fstab at the ssh connection destination and execute a command
I want to separate the processing between test time and production environment
Python: I want to measure the processing time of a function neatly
I want to make a web application using React and Python flask
I want to make matplotlib a dark theme
I want to make a game with Python
I want to write to a file with Python
I want to receive the configuration file and check if the JSON file generated by jinja2 is a valid JSON
[Hi Py (Part 1)] I want to make something for the time being, so first set a goal.
The story of IPv6 address that I want to keep at a minimum
I want to drop a file on tkinter and get its path [Tkinter DnD2]
I want to write an element to a file with numpy and check it.
Make sure to align the pre-processing at the time of forecast model creation and forecast
I want to add silence to the beginning of a wav file for 1 second
I want to get the file name, line number, and function name in Python 3.4
I want to see the file name from DataLoader
Visualize data and understand correlation at the same time
I want to randomly sample a file in Python
[Python] I want to make a nested list a tuple
I want to publish the product at the lowest cost
I want to replace the variables in the python template file and mass-produce it in another file.
I want to create a lunch database [EP1] Django study for the first time
I want to create a lunch database [EP1-4] Django study for the first time
wxPython: Draw animation and graph drawing at the same time
I want to move selenium for the time being [for mac]
I want to make a blog editor with django admin
I want to make a click macro with pyautogui (desire)
I want to make a click macro with pyautogui (outlook)
I tried to illustrate the time and time in C language
I tried to display the time and today's weather w
I want to know the features of Python and pip
I want to make the Dictionary type in the List unique
I want to map the EDINET code and securities number
I want to make input () a nice complement in python
I tried to automatically post to ChatWork at the time of deployment with fabric and ChatWork Api
How to start the PC at a fixed time every morning and execute the python program
Steps to change table and column names in your Django model at the same time
I tried the same data analysis with kaggle notebook (python) and Power BI at the same time ①
I want to see the graph in 3D! I can make such a dream come true.
I want to find the intersection of a Bezier curve and a straight line (Bezier Clipping method)
I want to make a voice changer using Python and SPTK with reference to a famous site
I tried to make a regular expression of "time" using Python
How to make a command to read the configuration file with pyramid
I want to create a system to prevent forgetting to tighten the key 1
I tried to make a periodical process with Selenium and Python
Scraping and tabelog ~ I want to find a good restaurant! ~ (Work)
[Introduction] I want to make a Mastodon Bot with Python! 【Beginners】
I want to create a pipfile and reflect it in docker
I want to make a parameter list from CloudFormation code (yaml)
I want to make the second line the column name in pandas
I want to connect remotely to another computer, and the nautilus command
For the time being, I want to convert files with ffmpeg !!
Browse .loc and .iloc at the same time in pandas DataFrame
I want to hack Robomaster S1 ① Rooting and file configuration check
I want to create a histogram and overlay the normal distribution curve on it. matplotlib edition
I tried to find out the difference between A + = B and A = A + B in Python, so make a note
[Twitter] I want to make the downloaded past tweets (of my account) into a beautiful CSV
I want to clear up the question of the "__init__" method and the "self" argument of a Python class.