Skip to content

Commit e02d227

Browse files
authored
Merge pull request #26 from 200sc/feature/noCgoAlsa
Feature/no cgo alsa
2 parents edd134f + 3db0330 commit e02d227

File tree

3 files changed

+143
-39
lines changed

3 files changed

+143
-39
lines changed

README.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ Waveform and Audio Synthesis library in Go
55
[![Go Report Card](https://goreportcard.com/badge/github.com/200sc/klangsynthese)](https://goreportcard.com/report/github.com/200sc/klangsynthese)
66

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

1110
## Usage
1211

@@ -17,29 +16,22 @@ See test files.
1716
| OS | Load | Modify | Save | Play |
1817
| -------- | ---- | ------ | ------ | ---- |
1918
| Windows | X | X | | X |
20-
| Linux | X | X | | ? |
19+
| Linux | X | X | | X |
2120
| Darwin | X | X | | |
2221

23-
To develop with linux you'll need alsa:
24-
25-
`sudo apt-get install alsa-base libasound2-dev`
26-
27-
Binaries built with this will probably need alsa-base as well to run on Linux.
28-
2922
## Quick recipe for testing on Linux
3023

3124
This recipe should run the wav test on Linux:
3225

33-
sudo apt-get install alsa-base libasound2-dev
3426
go get github.com/200sc/klangsynthese
35-
go get github.com/stretchr/testify/assert
27+
go get github.com/stretchr/testify/require
3628
go test github.com/200sc/klangsynthese/wav
3729

3830
## Other features
3931

4032
- [x] Wav support
4133
- [x] Mp3 support
42-
- [x] Flac support?
34+
- [x] Flac support
4335
- [ ] Ogg support
4436
- [x] Creating waveforms (Sin, Square, Saw, ...)
4537
- [x] Filtering audio samples

audio/encode_linux.go

Lines changed: 135 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,88 @@
33
package audio
44

55
import (
6-
"github.com/oakmound/alsa-go"
76
"github.com/pkg/errors"
7+
"github.com/yobert/alsa"
88
)
99

1010
type alsaAudio struct {
1111
*Encoding
12-
*alsa.Handle
12+
*alsa.Device
13+
playAmount int
14+
playProgress int
15+
stopCh chan struct{}
16+
playing bool
17+
playCh chan error
18+
period int
1319
}
1420

1521
func (aa *alsaAudio) Play() <-chan error {
16-
ch := make(chan error)
22+
// If currently playing, restart
23+
if aa.playing {
24+
aa.playProgress = 0
25+
return aa.playCh
26+
}
27+
aa.playing = true
28+
aa.playCh = make(chan error)
1729
go func() {
18-
// Todo: loop? library does not export loop
19-
_, err := aa.Handle.Write(aa.Encoding.Data)
20-
ch <- err
30+
for {
31+
var data []byte
32+
if len(aa.Encoding.Data)-aa.playProgress <= aa.playAmount {
33+
data = aa.Encoding.Data[aa.playProgress:]
34+
if aa.Loop {
35+
delta := aa.playAmount - (len(aa.Encoding.Data) - aa.playProgress)
36+
data = append(data, aa.Encoding.Data[:delta]...)
37+
}
38+
} else {
39+
data = aa.Encoding.Data[aa.playProgress : aa.playProgress+aa.playAmount]
40+
}
41+
if len(data) != 0 {
42+
err := aa.Device.Write(data, aa.period)
43+
if err != nil {
44+
select {
45+
case aa.playCh <- err:
46+
default:
47+
}
48+
break
49+
}
50+
}
51+
aa.playProgress += aa.playAmount
52+
if aa.playProgress > len(aa.Encoding.Data) {
53+
if aa.Loop {
54+
aa.playProgress %= len(aa.Encoding.Data)
55+
} else {
56+
select {
57+
case aa.playCh <- nil:
58+
default:
59+
}
60+
break
61+
}
62+
}
63+
select {
64+
case <-aa.stopCh:
65+
select {
66+
case aa.playCh <- nil:
67+
default:
68+
}
69+
break
70+
default:
71+
}
72+
}
73+
aa.playing = false
74+
aa.playProgress = 0
2175
}()
22-
return ch
76+
return aa.playCh
2377
}
2478

2579
func (aa *alsaAudio) Stop() error {
26-
// Todo: don't just pause man, actually stop
27-
// library we are using does not export stop
28-
return aa.Pause()
80+
if aa.playing {
81+
go func() {
82+
aa.stopCh <- struct{}{}
83+
}()
84+
} else {
85+
return errors.New("Audio not playing, cannot stop")
86+
}
87+
return nil
2988
}
3089

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

54113
func EncodeBytes(enc Encoding) (Audio, error) {
55-
handle := alsa.New()
56-
err := handle.Open("default", alsa.StreamTypePlayback, alsa.ModeBlock)
114+
handle, err := openDevice()
57115
if err != nil {
58116
return nil, err
59117
}
60-
61-
handle.SampleFormat = alsaFormat(enc.Bits)
62-
handle.SampleRate = int(enc.SampleRate)
63-
handle.Channels = int(enc.Channels)
64-
err = handle.ApplyHwParams()
118+
// Todo: annotate these errors with more info
119+
format, err := alsaFormat(enc.Bits)
120+
if err != nil {
121+
return nil, err
122+
}
123+
_, err = handle.NegotiateFormat(format)
124+
if err != nil {
125+
return nil, err
126+
}
127+
_, err = handle.NegotiateRate(int(enc.SampleRate))
128+
if err != nil {
129+
return nil, err
130+
}
131+
_, err = handle.NegotiateChannels(int(enc.Channels))
132+
if err != nil {
133+
return nil, err
134+
}
135+
// Default value at recommendation of library
136+
period, err := handle.NegotiatePeriodSize(2048)
137+
if err != nil {
138+
return nil, err
139+
}
140+
_, err = handle.NegotiateBufferSize(4096)
141+
if err != nil {
142+
return nil, err
143+
}
144+
err = handle.Prepare()
65145
if err != nil {
66146
return nil, err
67147
}
68148
return &alsaAudio{
69-
&enc,
70-
handle,
149+
playAmount: period * int(enc.Bits) / 4,
150+
period: period,
151+
Encoding: &enc,
152+
Device: handle,
153+
stopCh: make(chan struct{}),
71154
}, nil
72155
}
73156

74-
func alsaFormat(bits uint16) alsa.SampleFormat {
157+
func openDevice() (*alsa.Device, error) {
158+
cards, err := alsa.OpenCards()
159+
if err != nil {
160+
return nil, err
161+
}
162+
for i, c := range cards {
163+
dvcs, err := c.Devices()
164+
if err != nil {
165+
alsa.CloseCards([]*alsa.Card{c})
166+
continue
167+
}
168+
for _, d := range dvcs {
169+
if d.Type != alsa.PCM || !d.Play {
170+
continue
171+
}
172+
err := d.Open()
173+
if err != nil {
174+
continue
175+
}
176+
// We've a found a device we can hypothetically use
177+
// Close all other cards
178+
alsa.CloseCards(cards[i+1:])
179+
return d, nil
180+
}
181+
alsa.CloseCards([]*alsa.Card{c})
182+
}
183+
return nil, errors.New("No valid device found")
184+
}
185+
186+
func alsaFormat(bits uint16) (alsa.FormatType, error) {
75187
switch bits {
76188
case 8:
77-
return alsa.SampleFormatS8
189+
return alsa.S8, nil
78190
case 16:
79-
return alsa.SampleFormatS16LE
191+
return alsa.S16_LE, nil
80192
}
81-
return alsa.SampleFormatUnknown
193+
return 0, errors.New("Undefined alsa format for encoding bits")
82194
}

wav/wav_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ import (
66
"testing"
77
"time"
88

9-
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
1010
)
1111

1212
func TestBasicWav(t *testing.T) {
1313
fmt.Println("Running Basic Wav")
1414
f, err := os.Open("test.wav")
1515
fmt.Println(f)
16-
assert.Nil(t, err)
16+
require.Nil(t, err)
1717
a, err := Load(f)
18-
assert.Nil(t, err)
18+
require.Nil(t, err)
1919
err = <-a.Play()
20-
assert.Nil(t, err)
20+
require.Nil(t, err)
2121
time.Sleep(4 * time.Second)
2222
// In addition to the error tests here, this should play noise
2323
}

0 commit comments

Comments
 (0)