├── .gitignore ├── LICENSE ├── README.md ├── combined.png ├── combined2.png ├── examples ├── go.work └── main.go ├── go.mod ├── go.sum ├── waveform.go └── waveform_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | test-*.png 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Philippe Anel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-waveform 2 | 3 | Go module to generate and render waveform images from audio files 4 | 5 | Example : 6 | 7 | ![combined](combined.png) 8 | 9 | We can zoom : 10 | 11 | ![combined2](combined2.png) 12 | 13 | # TODO 14 | 15 | * doc 16 | * add range 17 | * add segment 18 | * add labels 19 | 20 | # Usage : 21 | 22 | ```go 23 | import ( 24 | // ... 25 | "github.com/xigh/go-waveform" 26 | "github.com/xigh/go-wavreader" 27 | ) 28 | 29 | func main() { 30 | r, err := os.Open("test-hello.wav") 31 | if err != nil { 32 | return err 33 | } 34 | defer r.Close() 35 | 36 | w0, err := wavreader.New(r) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | img := waveform.MinMax(w1, &waveform.Options{ 42 | Width: 1800, 43 | Height: 400, 44 | Zoom: 1.7, 45 | Half: false, 46 | MarginL: *margin, 47 | MarginR: *margin, 48 | MarginT: *margin, 49 | MarginB: *margin, 50 | Front: &color.NRGBA{ 51 | R: 255, 52 | G: 128, 53 | B: 0, 54 | A: 150, 55 | }, 56 | Back: &color.NRGBA{ 57 | A: 0, 58 | }, 59 | }) 60 | 61 | w, err := os.Create("test-minmax.png") 62 | if err != nil { 63 | return err 64 | } 65 | defer w.Close() 66 | 67 | err = png.Encode(w, img) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /combined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xigh/go-waveform/68c9c9aa60f575b661676b904a9737f3b7e34217/combined.png -------------------------------------------------------------------------------- /combined2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xigh/go-waveform/68c9c9aa60f575b661676b904a9737f3b7e34217/combined2.png -------------------------------------------------------------------------------- /examples/go.work: -------------------------------------------------------------------------------- 1 | go 1.20 2 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image" 7 | "image/color" 8 | "image/draw" 9 | "image/png" 10 | "log" 11 | "os" 12 | 13 | "github.com/xigh/go-waveform" 14 | "github.com/xigh/go-wavreader" 15 | "golang.org/x/image/font" 16 | "golang.org/x/image/font/basicfont" 17 | "golang.org/x/image/math/fixed" 18 | ) 19 | 20 | var ( 21 | wavStart = flag.Float64("start", 0.0, "set start (in seconds)") 22 | wavEnd = flag.Float64("end", -1.0, "set end (in seconds)") 23 | margin = flag.Int("margin", 50, "set margins in pixels") 24 | segLen = flag.Float64("seg", 0.1, "set segment length (in seconds)") 25 | rangeL = flag.Float64("range-start", -1.0, "set range left (in seconds)") 26 | rangeR = flag.Float64("range-end", -1.0, "set range right (in seconds)") 27 | ) 28 | 29 | func main() { 30 | flag.Parse() 31 | 32 | for i := 0; i < flag.NArg(); i++ { 33 | err := processWav(flag.Arg(i)) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | } 38 | } 39 | 40 | func addLabel(img *image.NRGBA, col color.NRGBA, x, y int, label string) { 41 | point := fixed.Point26_6{ 42 | X: fixed.Int26_6(x * 64), 43 | Y: fixed.Int26_6(y * 64), 44 | } 45 | 46 | d := &font.Drawer{ 47 | Dst: img, 48 | Src: image.NewUniform(col), 49 | Face: basicfont.Face7x13, 50 | Dot: point, 51 | } 52 | adv := d.MeasureString(label) 53 | d.Dot.X -= adv / 2 54 | d.DrawString(label) 55 | } 56 | 57 | func processWav(wavFile string) error { 58 | r, err := os.Open(wavFile) 59 | if err != nil { 60 | return err 61 | } 62 | defer r.Close() 63 | 64 | w0, err := wavreader.New(r) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | n := w0.Len() 70 | s := w0.Duration().Seconds() 71 | if *wavStart > s || *wavStart < 0 { 72 | return fmt.Errorf("invalid start %.2fs (duration: %.2fs)", *wavStart, s) 73 | } 74 | if *wavEnd > s { 75 | return fmt.Errorf("invalid end %.2fs (duration: %.2fs)", *wavEnd, s) 76 | } 77 | e0 := s 78 | if *wavEnd > 0 { 79 | e0 = *wavEnd 80 | } 81 | s0 := *wavStart 82 | if e0 < s0 { 83 | return fmt.Errorf("end < start") 84 | } 85 | 86 | r0 := w0.Rate() 87 | n0 := uint64(s0 * float64(n) / s) 88 | n1 := uint64(e0 * float64(n) / s) 89 | fmt.Printf("r0: %d, n: %d - s0: %.2f, e0: %.2f - n0: %d, n1: %d\n", r0, n, s0, e0, n0, n1) 90 | w1, err := w0.Slice(n0, n1) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | // --- 96 | 97 | im := waveform.MinMax(w1, &waveform.Options{ 98 | Width: 1800, 99 | Height: 400, 100 | Zoom: 1.7, 101 | Half: false, 102 | MarginL: *margin, 103 | MarginR: *margin, 104 | MarginT: *margin, 105 | MarginB: *margin, 106 | Front: &color.NRGBA{ 107 | R: 255, 108 | G: 128, 109 | B: 0, 110 | A: 150, 111 | }, 112 | Back: &color.NRGBA{ 113 | A: 0, 114 | }, 115 | }) 116 | 117 | wm, err := os.Create("test-minmax.png") 118 | if err != nil { 119 | return err 120 | } 121 | defer wm.Close() 122 | 123 | err = png.Encode(wm, im) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | // --- 129 | 130 | ia := waveform.AbsMax(w1, &waveform.Options{ 131 | Width: 1800, 132 | Height: 400, 133 | Zoom: 1.7, 134 | Half: false, 135 | MarginL: *margin, 136 | MarginR: *margin, 137 | MarginT: *margin, 138 | MarginB: *margin, 139 | Front: &color.NRGBA{ 140 | R: 50, 141 | G: 100, 142 | B: 200, 143 | A: 255, 144 | }, 145 | Back: &color.NRGBA{ 146 | A: 0, 147 | }, 148 | }) 149 | 150 | wa, err := os.Create("test-absmax.png") 151 | if err != nil { 152 | return err 153 | } 154 | defer wm.Close() 155 | 156 | err = png.Encode(wa, ia) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | // --- 162 | 163 | ir := waveform.Rms(w1, &waveform.Options{ 164 | Width: 1800, 165 | Height: 400, 166 | Zoom: 1.3, 167 | Half: false, 168 | MarginL: *margin, 169 | MarginR: *margin, 170 | MarginT: *margin, 171 | MarginB: *margin, 172 | Front: &color.NRGBA{ 173 | R: 100, 174 | G: 150, 175 | B: 250, 176 | A: 255, 177 | }, 178 | Back: &color.NRGBA{ 179 | A: 0, 180 | }, 181 | }) 182 | 183 | wr, err := os.Create("test-rms.png") 184 | if err != nil { 185 | return err 186 | } 187 | defer wr.Close() 188 | 189 | err = png.Encode(wr, ir) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | // --- 195 | rc := ia.Bounds() 196 | idx := rc.Dx() 197 | idy := rc.Dy() 198 | 199 | img := image.NewNRGBA(rc) 200 | 201 | // fill with checkerboard 202 | for y := 0; y < idy; y++ { 203 | for x := 0; x < idx; x++ { 204 | c := color.NRGBA{ 205 | R: 20, 206 | G: 20, 207 | B: 20, 208 | A: 255, 209 | } 210 | nx := x / 10 211 | ny := y / 10 212 | if (nx+ny)%2 == 0 { 213 | c = color.NRGBA{ 214 | R: 30, 215 | G: 30, 216 | B: 30, 217 | A: 255, 218 | } 219 | } 220 | img.SetNRGBA(x, y, c) 221 | } 222 | } 223 | 224 | dx := idx - *margin*2 225 | dy := idy - *margin*2 226 | 227 | t1 := w1.Duration().Seconds() 228 | fmt.Printf("sample-duration: %.3fs\n", t1) 229 | fmt.Printf("sample-rate: %d\n", w1.Rate()) 230 | fmt.Printf("pixels: %d\n", dx) 231 | 232 | draw.Draw(img, rc, ia, image.ZP, draw.Over) 233 | draw.Draw(img, rc, ir, image.ZP, draw.Over) 234 | draw.Draw(img, rc, im, image.ZP, draw.Over) 235 | 236 | if *rangeL >= 0 || *rangeR >= 0 { 237 | if *rangeL > *rangeR { 238 | return fmt.Errorf("rangeL > rangeR") 239 | } 240 | 241 | if *rangeL > t1 { 242 | return fmt.Errorf("rangeL > end") 243 | } 244 | 245 | if *rangeR > t1 { 246 | return fmt.Errorf("rangeR > end") 247 | } 248 | 249 | rng := image.NewNRGBA(rc) 250 | 251 | fmt.Printf("range-start: %.3fs\n", *rangeL) 252 | fmt.Printf("range-end: %.3fs\n", *rangeR) 253 | 254 | x0 := int((*rangeL / t1) * float64(dx)) 255 | x1 := int((*rangeR / t1) * float64(dx)) 256 | 257 | c := color.NRGBA{ 258 | R: 50, 259 | G: 150, 260 | B: 150, 261 | A: 100, 262 | } 263 | for x := x0; x < x1; x++ { 264 | for y := 0; y < dy; y++ { 265 | rng.SetNRGBA(x+*margin, y+*margin, c) 266 | } 267 | } 268 | 269 | draw.Draw(img, rc, rng, image.ZP, draw.Over) 270 | 271 | col := color.NRGBA{250, 150, 100, 220} 272 | addLabel(img, col, x0+*margin, *margin-5, fmt.Sprintf("%.3f", *rangeL)) 273 | addLabel(img, col, x1+*margin, *margin-5, fmt.Sprintf("%.3f", *rangeR)) 274 | } 275 | 276 | if *segLen > 0 { 277 | s1 := t1 / *segLen 278 | tx := int(float64(dx) / s1) 279 | // fmt.Printf("%d samples per 10ms\n", 10*w1.Rate()/1000) 280 | // fmt.Printf("%d pixels per 10ms\n", tx) 281 | 282 | for x := 0; x < dx; x++ { 283 | c := color.NRGBA{ 284 | R: 100, 285 | G: 100, 286 | B: 100, 287 | A: 255, 288 | } 289 | if (x/tx)%2 != 0 { 290 | c = color.NRGBA{ 291 | R: 200, 292 | G: 200, 293 | B: 200, 294 | A: 255, 295 | } 296 | } 297 | for y := dy - 3; y < dy; y++ { 298 | img.SetNRGBA(x+*margin, y+*margin, c) 299 | } 300 | } 301 | } 302 | 303 | if *segLen > 0 { 304 | col := color.NRGBA{250, 120, 200, 200} 305 | s1 := t1 / *segLen 306 | tx := int(float64(dx) / s1) 307 | 308 | if tx > 50 { 309 | s := 0.0 310 | for x := 0; x <= dx; x += tx { 311 | addLabel(img, col, x+*margin, *margin+dy+20, fmt.Sprintf("%.3f", s)) 312 | 313 | for y := 0; y < 5; y++ { 314 | img.SetNRGBA(x+*margin, *margin+dy+y, col) 315 | } 316 | s += *segLen 317 | } 318 | } 319 | 320 | for y := -1; y <= dy; y++ { 321 | img.SetNRGBA(*margin, *margin+y, col) 322 | if (y+1)%(dy/10) == 0 { 323 | for x := 0; x < 5; x++ { 324 | img.SetNRGBA(*margin-x, *margin+y, col) 325 | } 326 | } 327 | } 328 | } 329 | 330 | sr, err := os.Create("test-combined.png") 331 | if err != nil { 332 | return err 333 | } 334 | defer sr.Close() 335 | 336 | err = png.Encode(sr, img) 337 | if err != nil { 338 | return err 339 | } 340 | 341 | return nil 342 | } 343 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xigh/go-waveform 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/xigh/go-wavreader v0.0.0-20210516212152-f49019aa7352 7 | golang.org/x/image v0.23.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/xigh/go-wavreader v0.0.0-20210516212152-f49019aa7352 h1:iK7SnIgPre4wrUN7HpIhZpfP7884mcc7CQS3FeyGTes= 2 | github.com/xigh/go-wavreader v0.0.0-20210516212152-f49019aa7352/go.mod h1:SiPEYpvfimzIRkLfaOYXVNGbQGVmgQZ6BQHcKyj/iic= 3 | golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= 4 | golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= 5 | -------------------------------------------------------------------------------- /waveform.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "log" 7 | "math" 8 | ) 9 | 10 | // WaveReader interface ... 11 | type WaveReader interface { 12 | Len() uint64 13 | Rate() uint32 14 | Chans() uint16 15 | At(ch uint, offset uint64) (float32, error) 16 | } 17 | 18 | // Options represents ... 19 | type Options struct { 20 | Width int 21 | Height int 22 | Half bool 23 | Zoom float32 24 | MarginL int 25 | MarginR int 26 | MarginT int 27 | MarginB int 28 | Back *color.NRGBA 29 | Front *color.NRGBA 30 | } 31 | 32 | func initOptions(o *Options) *Options { 33 | no := &Options{ 34 | Width: 800, 35 | Height: 250, 36 | Zoom: 0.8, 37 | Front: &color.NRGBA{ 38 | R: 255, 39 | G: 255, 40 | B: 255, 41 | A: 255, 42 | }, 43 | Back: &color.NRGBA{ 44 | R: 0, 45 | G: 0, 46 | B: 0, 47 | A: 255, 48 | }, 49 | } 50 | if o != nil { 51 | if o.Half { 52 | no.Half = true 53 | } 54 | if o.Width > 0 { 55 | no.Width = o.Width 56 | } 57 | if o.Height > 0 { 58 | no.Height = o.Height 59 | } 60 | if o.Back != nil { 61 | no.Back = o.Back 62 | } 63 | if o.Front != nil { 64 | no.Front = o.Front 65 | } 66 | if o.Zoom != 0 { 67 | no.Zoom = o.Zoom 68 | } 69 | if o.MarginL >= 0 { 70 | no.MarginL = o.MarginL 71 | } 72 | if o.MarginR >= 0 { 73 | no.MarginR = o.MarginR 74 | } 75 | if o.MarginT >= 0 { 76 | no.MarginT = o.MarginT 77 | } 78 | if o.MarginB >= 0 { 79 | no.MarginB = o.MarginB 80 | } 81 | } 82 | return no 83 | } 84 | 85 | func newNRGBA(o *Options) *image.NRGBA { 86 | if o == nil { 87 | panic("options 'o' is nil") 88 | } 89 | dx := o.Width + o.MarginL + o.MarginR 90 | dy := o.Height + o.MarginT + o.MarginB 91 | rc := image.Rect(0, 0, dx, dy) 92 | im := image.NewNRGBA(rc) 93 | for y := 0; y < dx; y++ { 94 | for x := 0; x < dy; x++ { 95 | im.SetNRGBA(x, y, *o.Back) 96 | } 97 | } 98 | return im 99 | } 100 | 101 | func getMinMax(w WaveReader, pos, size int) (float32, float32) { 102 | s := float64(w.Len()) 103 | f := float64(pos) / float64(size) 104 | t := float64(pos+1) / float64(size) 105 | 106 | fo := uint64(f * s) 107 | to := uint64(t * s) 108 | min := float32(+1.0) 109 | max := float32(-1.0) 110 | for o := fo; o < to; o++ { 111 | h, err := w.At(0, o) 112 | if err != nil { 113 | log.Fatalf("w.At %d failed: %v", o, err) 114 | } 115 | if h < min { 116 | min = h 117 | } 118 | if h > max { 119 | max = h 120 | } 121 | } 122 | return min, max 123 | } 124 | 125 | // MinMax function ... 126 | func MinMax(w WaveReader, o *Options) *image.NRGBA { 127 | o = initOptions(o) 128 | wf := newNRGBA(o) 129 | if uint64(o.Width) < w.Len() { 130 | for x := 0; x < o.Width; x++ { 131 | h0, h1 := getMinMax(w, x, o.Width) 132 | m := float32(o.Height) / 2 133 | H0 := h0 * m // must be negative 134 | H1 := h1 * m 135 | t := m - H1*o.Zoom 136 | b := m - H0*o.Zoom 137 | for y := int(t); y < int(b); y++ { 138 | wf.SetNRGBA(x+o.MarginL, y+o.MarginT, *o.Front) 139 | } 140 | } 141 | return wf 142 | } 143 | return wf 144 | } 145 | 146 | func getAbsMax(w WaveReader, pos, size int) float32 { 147 | s := float64(w.Len()) 148 | f := float64(pos) / float64(size) 149 | t := float64(pos+1) / float64(size) 150 | 151 | fo := uint64(f * s) 152 | to := uint64(t * s) 153 | m := float32(0.0) 154 | for o := fo; o < to; o++ { 155 | h, err := w.At(0, o) 156 | if err != nil { 157 | log.Fatalf("w.At %d failed: %v", o, err) 158 | } 159 | if h < 0 { 160 | h = -h 161 | } 162 | if h > m { 163 | m = h 164 | } 165 | } 166 | return m 167 | } 168 | 169 | // AbsMax function ... 170 | func AbsMax(w WaveReader, o *Options) *image.NRGBA { 171 | o = initOptions(o) 172 | wf := newNRGBA(o) 173 | if uint64(o.Width) < w.Len() { 174 | for x := 0; x < o.Width; x++ { 175 | h := getAbsMax(w, x, o.Width) 176 | m := float32(o.Height) / 2 177 | H := h * m 178 | t := m - H*o.Zoom 179 | b := m + H*o.Zoom 180 | if o.Half { 181 | b = float32(o.Height) 182 | t = b - h*b*o.Zoom 183 | } 184 | for y := int(t); y < int(b); y++ { 185 | wf.SetNRGBA(x+o.MarginL, y+o.MarginT, *o.Front) 186 | } 187 | } 188 | return wf 189 | } 190 | return wf 191 | } 192 | 193 | func getRms(w WaveReader, pos, size int) float32 { 194 | s := float64(w.Len()) 195 | f := float64(pos) / float64(size) 196 | t := float64(pos+1) / float64(size) 197 | 198 | fo := uint64(f * s) 199 | to := uint64(t * s) 200 | ss := float32(0.0) 201 | for o := fo; o < to; o++ { 202 | h, err := w.At(0, o) 203 | if err != nil { 204 | log.Fatalf("w.At %d failed: %v", o, err) 205 | } 206 | ss += h * h 207 | } 208 | ss /= float32(to - fo) 209 | sq := math.Sqrt(float64(ss)) 210 | return float32(sq) 211 | } 212 | 213 | // Rms function ... 214 | func Rms(w WaveReader, o *Options) *image.NRGBA { 215 | o = initOptions(o) 216 | wf := newNRGBA(o) 217 | if uint64(o.Width) < w.Len() { 218 | for x := 0; x < o.Width; x++ { 219 | h := getRms(w, x, o.Width) 220 | m := float32(o.Height) / 2 221 | H := h * m 222 | t := m - H*o.Zoom 223 | b := m + H*o.Zoom 224 | if o.Half { 225 | b = float32(o.Height) 226 | t = b - h*b*o.Zoom 227 | } 228 | for y := int(t); y < int(b); y++ { 229 | wf.SetNRGBA(x+o.MarginL, y+o.MarginT, *o.Front) 230 | } 231 | } 232 | return wf 233 | } 234 | return wf 235 | } 236 | -------------------------------------------------------------------------------- /waveform_test.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "image/png" 7 | "math/rand" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | type dummyWave []float32 13 | 14 | func newDummyWave() dummyWave { 15 | w := make([]float32, 48000) 16 | src := rand.NewSource(0) 17 | rnd := rand.New(src) 18 | for n := 0; n < len(w); n++ { 19 | w[n] = rnd.Float32() 20 | } 21 | return w 22 | } 23 | 24 | func (dw dummyWave) Len() uint64 { 25 | return uint64(len(dw)) 26 | } 27 | 28 | func (dw dummyWave) Rate() uint32 { 29 | return 48000 30 | } 31 | 32 | func (dw dummyWave) Chans() uint16 { 33 | return 1 34 | } 35 | 36 | func (dw dummyWave) At(ch uint, offset uint64) (float32, error) { 37 | if ch != 0 { 38 | return 0.0, fmt.Errorf("invalid channel") 39 | } 40 | return dw[offset], nil 41 | } 42 | 43 | func TestMax(t *testing.T) { 44 | dw := newDummyWave() 45 | im := MinMax(dw, &Options{ 46 | Width: 1800, 47 | Front: &color.NRGBA{ 48 | R: 50, 49 | G: 100, 50 | B: 200, 51 | A: 255, 52 | }, 53 | Back: &color.NRGBA{ 54 | A: 0, 55 | }, 56 | }) 57 | w, err := os.Create("test-max.png") 58 | if err != nil { 59 | t.Fatalf("create failed: %v", err) 60 | } 61 | err = png.Encode(w, im) 62 | if err != nil { 63 | t.Fatalf("png.Encode failed: %v", err) 64 | } 65 | } 66 | 67 | func TestMaxHalf(t *testing.T) { 68 | dw := newDummyWave() 69 | im := MinMax(dw, &Options{ 70 | Width: 1800, 71 | Half: true, 72 | Front: &color.NRGBA{ 73 | R: 10, 74 | G: 50, 75 | B: 250, 76 | A: 255, 77 | }, 78 | Back: &color.NRGBA{ 79 | A: 0, 80 | }, 81 | }) 82 | w, err := os.Create("test-max-half.png") 83 | if err != nil { 84 | t.Fatalf("create failed: %v", err) 85 | } 86 | err = png.Encode(w, im) 87 | if err != nil { 88 | t.Fatalf("png.Encode failed: %v", err) 89 | } 90 | } 91 | --------------------------------------------------------------------------------