Skip to content

Commit

Permalink
Merge pull request #26 from 200sc/feature/noCgoAlsa
Browse files Browse the repository at this point in the history
Feature/no cgo alsa
  • Loading branch information
200sc authored Jul 7, 2019
2 parents edd134f + 3db0330 commit e02d227
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 39 deletions.
16 changes: 4 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ Waveform and Audio Synthesis library in Go
[![Go Report Card](https://goreportcard.com/badge/github.com/200sc/klangsynthese)](https://goreportcard.com/report/github.com/200sc/klangsynthese)

Klangsynthese right now supports a number of features that will work regardless of OS,
and a number of features specific to Windows where the hope is to move support to Linux
and Darwin.
with further support planned for OSX as soon as we get our hands on one to test with.

## Usage

Expand All @@ -17,29 +16,22 @@ See test files.
| OS | Load | Modify | Save | Play |
| -------- | ---- | ------ | ------ | ---- |
| Windows | X | X | | X |
| Linux | X | X | | ? |
| Linux | X | X | | X |
| Darwin | X | X | | |

To develop with linux you'll need alsa:

`sudo apt-get install alsa-base libasound2-dev`

Binaries built with this will probably need alsa-base as well to run on Linux.

## Quick recipe for testing on Linux

This recipe should run the wav test on Linux:

sudo apt-get install alsa-base libasound2-dev
go get github.com/200sc/klangsynthese
go get github.com/stretchr/testify/assert
go get github.com/stretchr/testify/require
go test github.com/200sc/klangsynthese/wav

## Other features

- [x] Wav support
- [x] Mp3 support
- [x] Flac support?
- [x] Flac support
- [ ] Ogg support
- [x] Creating waveforms (Sin, Square, Saw, ...)
- [x] Filtering audio samples
Expand Down
158 changes: 135 additions & 23 deletions audio/encode_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,88 @@
package audio

import (
"github.com/oakmound/alsa-go"
"github.com/pkg/errors"
"github.com/yobert/alsa"
)

type alsaAudio struct {
*Encoding
*alsa.Handle
*alsa.Device
playAmount int
playProgress int
stopCh chan struct{}
playing bool
playCh chan error
period int
}

func (aa *alsaAudio) Play() <-chan error {
ch := make(chan error)
// If currently playing, restart
if aa.playing {
aa.playProgress = 0
return aa.playCh
}
aa.playing = true
aa.playCh = make(chan error)
go func() {
// Todo: loop? library does not export loop
_, err := aa.Handle.Write(aa.Encoding.Data)
ch <- err
for {
var data []byte
if len(aa.Encoding.Data)-aa.playProgress <= aa.playAmount {
data = aa.Encoding.Data[aa.playProgress:]
if aa.Loop {
delta := aa.playAmount - (len(aa.Encoding.Data) - aa.playProgress)
data = append(data, aa.Encoding.Data[:delta]...)
}
} else {
data = aa.Encoding.Data[aa.playProgress : aa.playProgress+aa.playAmount]
}
if len(data) != 0 {
err := aa.Device.Write(data, aa.period)
if err != nil {
select {
case aa.playCh <- err:
default:
}
break
}
}
aa.playProgress += aa.playAmount
if aa.playProgress > len(aa.Encoding.Data) {
if aa.Loop {
aa.playProgress %= len(aa.Encoding.Data)
} else {
select {
case aa.playCh <- nil:
default:
}
break
}
}
select {
case <-aa.stopCh:
select {
case aa.playCh <- nil:
default:
}
break
default:
}
}
aa.playing = false
aa.playProgress = 0
}()
return ch
return aa.playCh
}

func (aa *alsaAudio) Stop() error {
// Todo: don't just pause man, actually stop
// library we are using does not export stop
return aa.Pause()
if aa.playing {
go func() {
aa.stopCh <- struct{}{}
}()
} else {
return errors.New("Audio not playing, cannot stop")
}
return nil
}

func (aa *alsaAudio) Filter(fs ...Filter) (Audio, error) {
Expand All @@ -52,31 +111,84 @@ func (aa *alsaAudio) MustFilter(fs ...Filter) Audio {
}

func EncodeBytes(enc Encoding) (Audio, error) {
handle := alsa.New()
err := handle.Open("default", alsa.StreamTypePlayback, alsa.ModeBlock)
handle, err := openDevice()
if err != nil {
return nil, err
}

handle.SampleFormat = alsaFormat(enc.Bits)
handle.SampleRate = int(enc.SampleRate)
handle.Channels = int(enc.Channels)
err = handle.ApplyHwParams()
// Todo: annotate these errors with more info
format, err := alsaFormat(enc.Bits)
if err != nil {
return nil, err
}
_, err = handle.NegotiateFormat(format)
if err != nil {
return nil, err
}
_, err = handle.NegotiateRate(int(enc.SampleRate))
if err != nil {
return nil, err
}
_, err = handle.NegotiateChannels(int(enc.Channels))
if err != nil {
return nil, err
}
// Default value at recommendation of library
period, err := handle.NegotiatePeriodSize(2048)
if err != nil {
return nil, err
}
_, err = handle.NegotiateBufferSize(4096)
if err != nil {
return nil, err
}
err = handle.Prepare()
if err != nil {
return nil, err
}
return &alsaAudio{
&enc,
handle,
playAmount: period * int(enc.Bits) / 4,
period: period,
Encoding: &enc,
Device: handle,
stopCh: make(chan struct{}),
}, nil
}

func alsaFormat(bits uint16) alsa.SampleFormat {
func openDevice() (*alsa.Device, error) {
cards, err := alsa.OpenCards()
if err != nil {
return nil, err
}
for i, c := range cards {
dvcs, err := c.Devices()
if err != nil {
alsa.CloseCards([]*alsa.Card{c})
continue
}
for _, d := range dvcs {
if d.Type != alsa.PCM || !d.Play {
continue
}
err := d.Open()
if err != nil {
continue
}
// We've a found a device we can hypothetically use
// Close all other cards
alsa.CloseCards(cards[i+1:])
return d, nil
}
alsa.CloseCards([]*alsa.Card{c})
}
return nil, errors.New("No valid device found")
}

func alsaFormat(bits uint16) (alsa.FormatType, error) {
switch bits {
case 8:
return alsa.SampleFormatS8
return alsa.S8, nil
case 16:
return alsa.SampleFormatS16LE
return alsa.S16_LE, nil
}
return alsa.SampleFormatUnknown
return 0, errors.New("Undefined alsa format for encoding bits")
}
8 changes: 4 additions & 4 deletions wav/wav_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestBasicWav(t *testing.T) {
fmt.Println("Running Basic Wav")
f, err := os.Open("test.wav")
fmt.Println(f)
assert.Nil(t, err)
require.Nil(t, err)
a, err := Load(f)
assert.Nil(t, err)
require.Nil(t, err)
err = <-a.Play()
assert.Nil(t, err)
require.Nil(t, err)
time.Sleep(4 * time.Second)
// In addition to the error tests here, this should play noise
}

0 comments on commit e02d227

Please sign in to comment.