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