audio.go (view raw)
1package captcha
2
3import (
4 "bytes"
5 crand "crypto/rand"
6 "encoding/binary"
7 "math"
8 "os"
9 "rand"
10 "io"
11)
12
13const sampleRate = 8000
14
15var (
16 // Length of the longest number sound
17 longestNumSndLen int
18 endingBeepSound []byte
19)
20
21// mixSound mixes src into dst. Dst must have length equal to or greater than
22// src length.
23func mixSound(dst, src []byte) {
24 for i, v := range src {
25 av := int(v)
26 bv := int(dst[i])
27 if av < 128 && bv < 128 {
28 dst[i] = byte(av * bv / 128)
29 } else {
30 dst[i] = byte(2*(av+bv) - av*bv/128 - 256)
31 }
32 }
33}
34
35func setSoundLevel(a []byte, level float64) {
36 for i, v := range a {
37 av := float64(v)
38 switch {
39 case av > 128:
40 if av = (av-128)*level + 128; av < 128 {
41 av = 128
42 }
43 case av < 128:
44 if av = 128 - (128-av)*level; av > 128 {
45 av = 128
46 }
47 default:
48 continue
49 }
50 a[i] = byte(av)
51 }
52}
53
54// changeSpeed returns new PCM bytes from the bytes with the speed and pitch
55// changed to the given value that must be in range [0, x].
56func changeSpeed(a []byte, pitch float64) []byte {
57 b := make([]byte, int(math.Floor(float64(len(a))*pitch)))
58 var p float64
59 for _, v := range a {
60 for i := int(p); i < int(p+pitch); i++ {
61 b[i] = v
62 }
63 p += pitch
64 }
65 return b
66}
67
68// rndFloat64n returns a random float64 number in range [from, to].
69func rndFloat64n(from, to float64) float64 {
70 return (to-from)*rand.Float64() + from
71}
72
73func randomSpeed(a []byte) []byte {
74 pitch := rndFloat64n(0.9, 1.2)
75 return changeSpeed(a, pitch)
76}
77
78func makeSilence(length int) []byte {
79 b := make([]byte, length)
80 for i := 0; i < length; i++ {
81 b[i] = 128
82 }
83 return b
84}
85
86func makeStaticNoise(length int, level uint8) []byte {
87 noise := make([]byte, length)
88 _, err := io.ReadFull(crand.Reader, noise)
89 if err != nil {
90 panic("error reading from random source: " + err.String())
91 }
92 for i := 0; i < len(noise); i++ {
93 noise[i] %= level
94 noise[i] += 128 - level/2
95 }
96 return noise
97}
98
99func reversedSound(a []byte) []byte {
100 ln := len(a)
101 b := make([]byte, ln)
102 for i, v := range a {
103 b[ln-1-i] = v
104 }
105 return b
106}
107
108func makeBackgroundSound(length int) []byte {
109 b := makeStaticNoise(length, 8)
110 for i := 0; i < length/(sampleRate/10); i++ {
111 snd := numberSounds[rand.Intn(10)]
112 snd = changeSpeed(reversedSound(snd), rndFloat64n(0.8, 1.4))
113 place := rand.Intn(len(b) - len(snd))
114 setSoundLevel(snd, rndFloat64n(0.5, 1.2))
115 mixSound(b[place:], snd)
116 }
117 setSoundLevel(b, rndFloat64n(0.2, 0.3))
118 return b
119}
120
121func randomizedNumSound(n byte) []byte {
122 s := randomSpeed(numberSounds[n])
123 setSoundLevel(s, rndFloat64n(0.7, 1.3))
124 return s
125}
126
127func init() {
128 for _, v := range numberSounds {
129 if longestNumSndLen < len(v) {
130 longestNumSndLen = len(v)
131 }
132 }
133 endingBeepSound = changeSpeed(beepSound, 1.4)
134}
135
136type CaptchaAudio struct {
137 body *bytes.Buffer
138}
139
140func NewAudio(numbers []byte) *CaptchaAudio {
141 numsnd := make([][]byte, len(numbers))
142 nsdur := 0
143 for i, n := range numbers {
144 snd := randomizedNumSound(n)
145 nsdur += len(snd)
146 numsnd[i] = snd
147 }
148 // Intervals between numbers (including beginning)
149 intervals := make([]int, len(numbers)+1)
150 intdur := 0
151 for i := range intervals {
152 // 1 to 3 seconds
153 dur := rnd(sampleRate, sampleRate*3)
154 intdur += dur
155 intervals[i] = dur
156 }
157 // Background noise
158 bg := makeBackgroundSound(longestNumSndLen*len(numbers) + intdur)
159 // --
160 a := new(CaptchaAudio)
161 a.body = bytes.NewBuffer(nil)
162 // Prelude, three beeps
163 sil := makeSilence(sampleRate / 5)
164 a.body.Write(beepSound)
165 a.body.Write(sil)
166 a.body.Write(beepSound)
167 a.body.Write(sil)
168 a.body.Write(beepSound)
169 // Numbers
170 pos := intervals[0]
171 for i, v := range numsnd {
172 mixSound(bg[pos:], v)
173 pos += len(v) + intervals[i+1]
174 }
175 a.body.Write(bg)
176 // Ending
177 a.body.Write(endingBeepSound)
178 return a
179}
180
181// WriteTo writes captcha audio in WAVE format.
182func (a *CaptchaAudio) WriteTo(w io.Writer) (n int64, err os.Error) {
183 nn, err := w.Write(waveHeader)
184 n = int64(nn)
185 if err != nil {
186 return
187 }
188 err = binary.Write(w, binary.LittleEndian, uint32(a.body.Len()))
189 if err != nil {
190 return
191 }
192 nn += 4
193 n, err = a.body.WriteTo(w)
194 n += int64(nn)
195 return
196}