Skip to content

Commit eae966c

Browse files
committed
move audio.System.Load/Save to audio.Load and audio.Save
Code serializing/deserializing audio should not depend on actual audio.System implementation.
1 parent 8bf3631 commit eae966c

File tree

8 files changed

+190
-260
lines changed

8 files changed

+190
-260
lines changed

audio/audio.go

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
// Sound effects and music can be changed using functions: SetSfx and SetMusic.
88
package audio
99

10-
import "fmt"
10+
import (
11+
"fmt"
12+
)
1113

1214
// Play starts playing sound effect with given sfxNo on specified channel.
1315
//
@@ -63,30 +65,6 @@ func GetStat() Stat {
6365
return system.Stat()
6466
}
6567

66-
// Save stores audio system state to byte slice. State is stored in binary form.
67-
// The format is described in Synthesizer.Save source code.
68-
func Save() ([]byte, error) {
69-
return system.Save()
70-
}
71-
72-
// Load restores audio system state from byte slice. State is restored from binary form.
73-
// The format is described in Synthesizer.Save source code.
74-
func Load(b []byte) error {
75-
if err := system.Load(b); err != nil {
76-
return err
77-
}
78-
79-
for sfxNo := range Sfx {
80-
Sfx[sfxNo] = system.GetSfx(sfxNo)
81-
}
82-
83-
for patternNo := range Pat {
84-
Pat[patternNo] = system.GetMusic(patternNo)
85-
}
86-
87-
return nil
88-
}
89-
9068
var system System = &Synthesizer{}
9169

9270
type Stat struct {
@@ -288,6 +266,4 @@ type System interface {
288266
// GetMusic returns music pattern with given number. patterNo is 0-63. Trying to get
289267
// pattern number higher than 63 will result in returning empty Pattern (zero-value).
290268
GetMusic(patterNo int) Pattern
291-
Save() ([]byte, error)
292-
Load([]byte) error
293269
}

audio/memory.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
package audio
55

6+
import (
7+
"bytes"
8+
"fmt"
9+
)
10+
611
var (
712
Sfx [64]SoundEffect // Sound effects
813
Pat [64]Pattern // Music patterns
@@ -18,3 +23,132 @@ func Sync() {
1823
system.SetMusic(i, pattern)
1924
}
2025
}
26+
27+
// Save stores audio system state to byte slice. State is stored in binary form.
28+
func Save() ([]byte, error) {
29+
buffer := bytes.NewBuffer(nil)
30+
31+
buffer.WriteByte(schemaVersion)
32+
33+
for i := 0; i <= maxSfxNo; i++ {
34+
sfx := Sfx[i]
35+
for _, note := range sfx.Notes {
36+
buffer.WriteByte(byte(note.Pitch))
37+
buffer.WriteByte(byte(note.Instrument))
38+
buffer.WriteByte(byte(note.Volume))
39+
buffer.WriteByte(byte(note.Effect))
40+
}
41+
buffer.WriteByte(sfx.Speed)
42+
buffer.WriteByte(sfx.LoopStart)
43+
buffer.WriteByte(sfx.LoopStop)
44+
buffer.WriteByte(sfx.Detune)
45+
buffer.WriteByte(sfx.Reverb)
46+
buffer.WriteByte(sfx.Dampen)
47+
buffer.WriteByte(boolToByte(sfx.Noiz))
48+
buffer.WriteByte(boolToByte(sfx.Buzz))
49+
}
50+
51+
for i := 0; i <= maxPatternNo; i++ {
52+
pattern := Pat[i]
53+
for _, sfx := range pattern.Sfx {
54+
buffer.WriteByte(sfx.SfxNo)
55+
buffer.WriteByte(boolToByte(sfx.Enabled))
56+
}
57+
buffer.WriteByte(boolToByte(pattern.BeginLoop))
58+
buffer.WriteByte(boolToByte(pattern.EndLoop))
59+
buffer.WriteByte(boolToByte(pattern.StopAtTheEnd))
60+
}
61+
62+
return buffer.Bytes(), nil
63+
}
64+
65+
func boolToByte(b bool) byte {
66+
if b {
67+
return 1
68+
}
69+
70+
return 0
71+
}
72+
73+
// Load restores audio system state from byte slice. State is restored from binary form.
74+
func Load(state []byte) error {
75+
if len(state) == 0 {
76+
return fmt.Errorf("state is empty")
77+
}
78+
79+
version := state[0]
80+
if schemaVersion != version {
81+
return fmt.Errorf("state version %d is not supported. Only %d is supported.", version, schemaVersion)
82+
}
83+
84+
const expectedStateLen = 9409
85+
if len(state) != expectedStateLen {
86+
return fmt.Errorf("invalid length of state. Must be %d.", expectedStateLen)
87+
}
88+
89+
offset := 1
90+
91+
for sfxNo := 0; sfxNo <= maxSfxNo; sfxNo++ {
92+
var sfx SoundEffect
93+
94+
for j, note := range sfx.Notes {
95+
note.Pitch = Pitch(state[offset])
96+
offset++
97+
note.Instrument = Instrument(state[offset])
98+
offset++
99+
note.Volume = Volume(state[offset])
100+
offset++
101+
note.Effect = Effect(state[offset])
102+
offset++
103+
104+
sfx.Notes[j] = note
105+
}
106+
107+
sfx.Speed = state[offset]
108+
offset++
109+
sfx.LoopStart = state[offset]
110+
offset++
111+
sfx.LoopStop = state[offset]
112+
offset++
113+
sfx.Detune = state[offset]
114+
offset++
115+
sfx.Reverb = state[offset]
116+
offset++
117+
sfx.Dampen = state[offset]
118+
offset++
119+
sfx.Noiz = byteToBool(state[offset])
120+
offset++
121+
sfx.Buzz = byteToBool(state[offset])
122+
offset++
123+
124+
Sfx[sfxNo] = sfx
125+
}
126+
127+
for patterNo := 0; patterNo <= maxPatternNo; patterNo++ {
128+
var pattern Pattern
129+
130+
for j, sfx := range pattern.Sfx {
131+
sfx.SfxNo = state[offset]
132+
offset++
133+
sfx.Enabled = byteToBool(state[offset])
134+
offset++
135+
136+
pattern.Sfx[j] = sfx
137+
}
138+
139+
pattern.BeginLoop = byteToBool(state[offset])
140+
offset++
141+
pattern.EndLoop = byteToBool(state[offset])
142+
offset++
143+
pattern.StopAtTheEnd = byteToBool(state[offset])
144+
offset++
145+
146+
Pat[patterNo] = pattern
147+
}
148+
149+
return nil
150+
}
151+
152+
func byteToBool(b byte) bool {
153+
return b == 1
154+
}

audio/synth_bench_test.go renamed to audio/memory_bench_test.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@ import (
1010
"github.com/elgopher/pi/audio"
1111
)
1212

13-
func BenchmarkSynthesizer_Load(b *testing.B) {
13+
func BenchmarkLoad(b *testing.B) {
1414
b.ReportAllocs()
1515
b.ResetTimer()
1616

17-
s := audio.Synthesizer{}
18-
1917
for i := 0; i < b.N; i++ {
20-
err := s.Load(validSave) // 11us
18+
err := audio.Load(validSave) // 11us
2119
if err != nil {
2220
b.Logf("error returned from Synthesizer.Load: %s", err)
2321
b.Fail()
@@ -29,15 +27,13 @@ func BenchmarkSynthesizer_Save(b *testing.B) {
2927
b.ReportAllocs()
3028
b.ResetTimer()
3129

32-
s := audio.Synthesizer{}
33-
3430
var (
3531
bytes []byte
3632
err error
3733
)
3834

3935
for i := 0; i < b.N; i++ {
40-
bytes, err = s.Save() // 27us
36+
bytes, err = audio.Save() // 27us
4137
if err != nil {
4238
b.Logf("error returned from Synthesizer.Save: %s", err)
4339
b.Fail()

audio/memory_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// (c) 2022-2023 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package audio_test
5+
6+
import (
7+
_ "embed"
8+
"github.com/elgopher/pi/audio"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"testing"
12+
)
13+
14+
//go:embed "internal/valid-save"
15+
var validSave []byte
16+
17+
func TestSave(t *testing.T) {
18+
t.Run("should save in binary format", func(t *testing.T) {
19+
audio.Sfx[3] = validEffect
20+
audio.Pat[4] = validPattern
21+
bytes, err := audio.Save()
22+
require.NoError(t, err)
23+
assert.Equal(t, validSave, bytes)
24+
})
25+
}
26+
27+
func TestLoad(t *testing.T) {
28+
t.Run("should load state", func(t *testing.T) {
29+
err := audio.Load(validSave)
30+
require.NoError(t, err)
31+
assert.Equal(t, validEffect, audio.Sfx[3])
32+
assert.Equal(t, validPattern, audio.Pat[4])
33+
})
34+
35+
t.Run("should return error when state is empty", func(t *testing.T) {
36+
err := audio.Load([]byte{})
37+
assert.Error(t, err)
38+
})
39+
40+
t.Run("should return error when version is not supported", func(t *testing.T) {
41+
err := audio.Load([]byte{2})
42+
assert.Error(t, err)
43+
})
44+
45+
t.Run("should return error when state has invalid length", func(t *testing.T) {
46+
err := audio.Load([]byte{1, 0, 0, 0})
47+
assert.Error(t, err)
48+
})
49+
}

0 commit comments

Comments
 (0)