Skip to content

Refactored audio stack with PortAudio #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
go-version: '1.19.5'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: sudo apt-get update -q
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
- run: sudo apt-get install portaudio19-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
- run: OS=Linux ARCH=x86_64 VERSION=$VERSION make tar
- run: OS=Linux ARCH=x86_64 VERSION=$VERSION make deb
- run: sha256sum Ludo-Linux-x86_64-${VERSION}.tar.gz > Ludo-Linux-x86_64-${VERSION}.tar.gz.sha256
Expand Down Expand Up @@ -53,7 +53,7 @@ jobs:
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libgl1-mesa-dev:amd64 libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
- run: sudo apt install -f libgl1-mesa-dev:amd64 libc6-dev:armhf gcc-arm-linux-gnueabihf portaudio19-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
- run: export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/
- run: GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -v
- run: OS=Linux ARCH=arm VERSION=$VERSION make tar
Expand All @@ -80,7 +80,7 @@ jobs:
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install golang.org/x/lint/golint@latest
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: brew install openal-soft
- run: brew install portaudio
- run: echo ${{ secrets.OSXCERT }} | base64 --decode > dev.p12
- run: security create-keychain -p github build.keychain
- run: security default-keychain -s build.keychain
Expand Down Expand Up @@ -113,13 +113,15 @@ jobs:
go-version: '1.19.5'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: choco install wget make hashdeep --ignore-checksums
- run: wget --no-check-certificate http://www.openal-soft.org/openal-binaries/openal-soft-1.21.0-bin.zip
- run: wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe
- run: 7z x openal-soft-1.21.0-bin.zip -o/c/
- run: echo "CGO_CFLAGS=-I/c/openal-soft-1.21.0-bin/include/" >> $GITHUB_ENV
- run: wget --no-check-certificate http://files.portaudio.com/archives/pa_stable_v190700_20210406.tgz
- run: tar -xf pa_stable_v190700_20210406.tgz -C/c/
# Not sure if this work on windows; Build portaudio because it has no binary dists
- run: sh -c 'cd /c/portaudio/; ./configure; make'- run: echo "CGO_CFLAGS=-I/c/openal-soft-1.21.0-bin/include/" >> $GITHUB_ENV
- run: echo "CGO_LDFLAGS=-L/c/openal-soft-1.21.0-bin/libs/Win64/" >> $GITHUB_ENV
- run: cp /c/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/x86_64-w64-mingw32/lib/libwinpthread* .
- run: cp /c/openal-soft-1.21.0-bin/bin/Win64/soft_oal.dll OpenAL32.dll
# I'm not sure about this... Digged from configure, Needs more tests
- run: cp /c/portaudio/lib/.libs/portaudio.dll PortAudio.dll
- run: cp /c/Windows/System32/VCRUNTIME140.dll .
- run: mkdir -p ./Ludo-Windows-x86_64-${VERSION}/
- run: cp *.dll ./Ludo-Windows-x86_64-${VERSION}/
Expand Down
16 changes: 9 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- run: go install golang.org/x/lint/golint@latest
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: sudo apt-get update -q
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
- run: sudo apt-get install portaudio19-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
- run: go get .
- run: xvfb-run -a go test -v -race ./...
- run: go vet ./...
Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libgl1-mesa-dev:amd64 libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
- run: sudo apt install -f libgl1-mesa-dev:amd64 libc6-dev:armhf gcc-arm-linux-gnueabihf portaudio19-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
- run: export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/
- run: GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -v

Expand All @@ -68,7 +68,7 @@ jobs:
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install golang.org/x/lint/golint@latest
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: brew install openal-soft
- run: brew install portaudio
- run: go get .
- run: go test -v -race ./...
- run: go vet ./...
Expand All @@ -89,10 +89,12 @@ jobs:
- run: go install golang.org/x/lint/golint@latest
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: choco install wget --ignore-checksums
- run: wget --no-check-certificate http://www.openal-soft.org/openal-binaries/openal-soft-1.21.0-bin.zip
- run: 7z x openal-soft-1.21.0-bin.zip -o/c/
- run: echo "CGO_CFLAGS=-I/c/openal-soft-1.21.0-bin/include/" >> $GITHUB_ENV
- run: echo "CGO_LDFLAGS=-L/c/openal-soft-1.21.0-bin/libs/Win64/" >> $GITHUB_ENV
- run: wget --no-check-certificate http://files.portaudio.com/archives/pa_stable_v190700_20210406.tgz
- run: tar -xf pa_stable_v190700_20210406.tgz -C/c/
# Not sure if this work on windows; Build portaudio because it has no binary dists
- run: sh -c 'cd /c/portaudio/; ./configure; make'
- run: echo "CGO_CFLAGS=-I/c/portaudio/include/" >> $GITHUB_ENV
- run: echo "CGO_LDFLAGS=-L/c/portaudio/lib/" >> $GITHUB_ENV
- run: go get .
#- run: go test -v -race ./...
#- run: go vet ./...
Expand Down
159 changes: 80 additions & 79 deletions audio/audio.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,94 @@
package audio

import (
"encoding/binary"
"log"
"path/filepath"
"time"
"unsafe"

"github.com/gordonklaus/portaudio"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"golang.org/x/mobile/exp/audio/al"
)

const bufSize = 1024 * 8
const bufSize = 1024 * 4
const maxSeLen = 44100 * 8

var (
source al.Source
buffers []al.Buffer
rate int32
numBuffers int32
tmpBuf [bufSize]byte
tmpBufPtr int32
resPtr int32
paBuf [bufSize]int32
paSeBuf [maxSeLen]int32
paRate float64
paPtr int64
paPlayPtr int64
paSePtr int
paSeLen int
paStream *portaudio.Stream
paSeStream *portaudio.Stream
)

// Effects are sound effects
var Effects map[string]*Effect

// SetVolume sets the audio volume
func SetVolume(vol float32) {
source.SetGain(vol)
// PortAudio Callback
func paCallback(out []int32) {
for i := range out {
if paPlayPtr <= paPtr {
out[i] = int32(settings.Current.AudioVolume * float32(paBuf[paPlayPtr-(paPlayPtr/bufSize)*bufSize]))
paPlayPtr++
} else {
out[i] = 0
}

}
}

// Create PortAudio parameters
func NewParameters(out *portaudio.DeviceInfo) (p portaudio.StreamParameters) {
if out != nil {
p := &p.Output
p.Device = out
p.Channels = 2
if out.MaxOutputChannels < 2 {
p.Channels = out.MaxOutputChannels
}
p.Latency = out.DefaultLowOutputLatency
}
p.SampleRate = paRate / 2
p.FramesPerBuffer = portaudio.FramesPerBufferUnspecified
return p
}

// Init initializes the audio device
func Init() {
err := al.OpenDevice()
if err != nil {
log.Println(err)
err1 := portaudio.Initialize()
if err1 != nil {
log.Println(err1)
}

paRate = 44100
paPtr = 0
paPlayPtr = 0
paSePtr = 0
paSeLen = 0

h, _ := portaudio.DefaultOutputDevice()
paStream, _ = portaudio.OpenStream(NewParameters(h), paCallback)
paStream.Start()

paSeStream, _ = portaudio.OpenStream(NewParameters(h), func(out []int32) {
for i := range out {
if paSePtr < paSeLen {
out[i] = int32(settings.Current.MenuAudioVolume * float32(paSeBuf[paSePtr]))
paSePtr++
} else {
out[i] = 0
}

}
})
paSeStream.Start()
Effects = map[string]*Effect{}

assets := settings.Current.AssetsDirectory
Expand All @@ -55,18 +105,14 @@ func Init() {
// Reconfigure initializes the audio package. It sets the number of buffers, the
// volume and the source for the games.
func Reconfigure(r int32) {
rate = r
numBuffers = 4

log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", numBuffers, bufSize)

source = al.GenSources(1)[0]
buffers = al.GenBuffers(int(numBuffers))
resPtr = numBuffers
tmpBufPtr = 0
tmpBuf = [bufSize]byte{}

source.SetGain(settings.Current.AudioVolume)
paRate = float64(r)
paBuf = [bufSize]int32{}
paPtr = 0
paPlayPtr = 0
paStream.Close()
h, _ := portaudio.DefaultOutputDevice()
paStream, _ = portaudio.OpenStream(NewParameters(h), paCallback)
paStream.Start()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you can do some basic error management like

	if paStream != nil {
		if err := paStream.Close(); err != nil {
			log.Fatalln(err)
		}
	}
	h, err := portaudio.DefaultOutputDevice()
	if err != nil {
		log.Fatalln(err)
	}
	paStream, err = portaudio.OpenStream(NewParameters(h), paCallback)
	if err != nil {
		log.Fatalln(err)
	}
	if err = paStream.Start(); err != nil {
		log.Fatalln(err)
	}

}

func min(a, b int32) int32 {
Expand All @@ -76,66 +122,21 @@ func min(a, b int32) int32 {
return b
}

func alUnqueueBuffers() bool {
val := source.BuffersProcessed()

if val <= 0 {
return false
}

source.UnqueueBuffers(buffers[resPtr:val]...)
resPtr += val
return true
}

func alGetBuffer() al.Buffer {
if resPtr == 0 {
for {
if alUnqueueBuffers() {
break
}
time.Sleep(time.Millisecond)
}
}

resPtr--
return buffers[resPtr]
}

func fillInternalBuf(buf []byte) int32 {
readSize := min(bufSize-tmpBufPtr, int32(len(buf)))
copy(tmpBuf[tmpBufPtr:], buf[:readSize])
tmpBufPtr += readSize
return readSize
}

func write(buf []byte, size int32) int32 {
written := int32(0)

if state.FastForward {
return size
}

for size > 0 {

rc := fillInternalBuf(buf[written:])

written += rc
size -= rc

if tmpBufPtr != bufSize {
break
}

buffer := alGetBuffer()
time.Sleep(time.Millisecond * time.Duration((paPtr-paPlayPtr)/(bufSize/4)*8))

buffer.BufferData(al.FormatStereo16, tmpBuf[:], rate)
tmpBufPtr = 0
source.QueueBuffers(buffer)

if source.State() != al.Playing {
al.PlaySources(source)
}
mm := int(min(size/4, bufSize))
for i := 0; i < mm; i++ {
p := 4 * (int32(i))
paBuf[paPtr-(paPtr/bufSize)*bufSize] = int32(binary.LittleEndian.Uint32(buf[p : p+4]))
paPtr++
written += 4
}

return written
Expand Down
48 changes: 1 addition & 47 deletions audio/audio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,9 @@ import (
"testing"
)

func Test_alUnqueueBuffers(t *testing.T) {
t.Run("Return false if no buffers were processed", func(t *testing.T) {
got := alUnqueueBuffers()
if got {
t.Errorf("alUnqueueBuffers() = %v, want %v", got, false)
}
})
}

func Test_Sample(t *testing.T) {
t.Run("Doesn't crash when called", func(t *testing.T) {
Sample(-30000, -30000)
Sample( 30000, 30000)
Sample(30000, 30000)
})
}

func Test_fillInternalBuf(t *testing.T) {
Reconfigure(48000)
type args struct {
buf []byte
size int32
}
tests := []struct {
name string
args args
want int32
}{
{
name: "Fill the buffer partially",
args: args{
buf: make([]byte, bufSize),
size: 6000,
},
want: 6000,
},
{
name: "Fill the buffer fully",
args: args{
buf: make([]byte, bufSize),
size: 6000,
},
want: 2192,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fillInternalBuf(tt.args.buf[:tt.args.size]); got != tt.want {
t.Errorf("fillInternalBuf() = %v, want %v", got, tt.want)
}
})
}
}
Loading