├── test ├── tone16bit.flac ├── tone16bit.mp3 ├── tone16bit.ogg └── tone16bit.wav ├── .travis.yml ├── go.mod ├── samplereducefunc.go ├── LICENSE.md ├── cmd └── waveform │ ├── README.md │ └── waveform.go ├── samplereducefunc_test.go ├── waveform_example_test.go ├── go.sum ├── README.md ├── waveform_bench_test.go ├── colorfunc.go ├── colorfunc_test.go ├── waveform_test.go ├── options_test.go ├── options.go └── waveform.go /test/tone16bit.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/waveform/master/test/tone16bit.flac -------------------------------------------------------------------------------- /test/tone16bit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/waveform/master/test/tone16bit.mp3 -------------------------------------------------------------------------------- /test/tone16bit.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/waveform/master/test/tone16bit.ogg -------------------------------------------------------------------------------- /test/tone16bit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/waveform/master/test/tone16bit.wav -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.14 4 | - tip 5 | before_script: 6 | - go get -d ./... 7 | script: 8 | - go test -v ./... 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mdlayher/waveform 2 | 3 | go 1.14 4 | 5 | require ( 6 | azul3d.org/engine v0.0.0-20180624221640-25c8eab2d474 7 | github.com/mewkiz/flac v1.0.6 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /samplereducefunc.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "math" 5 | 6 | "azul3d.org/engine/audio" 7 | ) 8 | 9 | // SampleReduceFunc is a function which reduces a set of float64 audio samples 10 | // into a single float64 value. 11 | type SampleReduceFunc func(samples audio.Float64) float64 12 | 13 | // RMSF64Samples is a SampleReduceFunc which calculates the root mean square 14 | // of a slice of float64 audio samples, enabling the measurement of magnitude 15 | // over the entire set of samples. 16 | // 17 | // Derived from: http://en.wikipedia.org/wiki/Root_mean_square. 18 | func RMSF64Samples(samples audio.Float64) float64 { 19 | // Square and sum all input samples 20 | var sumSquare float64 21 | for i := range samples { 22 | sumSquare += math.Pow(samples.At(i), 2) 23 | } 24 | 25 | // Multiply squared sum by length of samples slice, return square root 26 | return math.Sqrt(sumSquare / float64(samples.Len())) 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (C) 2014 Matt Layher 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /cmd/waveform/README.md: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | To install and use `waveform`, simply run: 5 | 6 | ``` 7 | $ go install github.com/mdlayher/waveform/... 8 | ``` 9 | 10 | The `waveform` binary is now installed in your `$GOPATH`. It has several options available 11 | for generating waveform images: 12 | 13 | ``` 14 | $ waveform -h 15 | Usage of waveform: 16 | -alt="": hex alternate color of output waveform image 17 | -bg="#FFFFFF": hex background color of output waveform image 18 | -fg="#000000": hex foreground color of output waveform image 19 | -fn="solid": function used to color output waveform image [options: fuzz, gradient, solid, stripe] 20 | -resolution=1: number of times audio is read and drawn per second of audio 21 | -sharpness=1: sharpening factor used to add curvature to a scaled image 22 | -x=1: scaling factor for image X-axis 23 | -y=1: scaling factor for image Y-axis 24 | ``` 25 | 26 | `waveform` currently supports both WAV and FLAC audio files. An audio stream must 27 | be passed on `stdin`, and the resulting, PNG-encoded image will be written to `stdout`. 28 | Any errors which occur will be written to `stderr`. 29 | -------------------------------------------------------------------------------- /samplereducefunc_test.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "azul3d.org/engine/audio" 8 | ) 9 | 10 | // TestRMSF64Samples verifies that RMSF64Samples computes correct results 11 | func TestRMSF64Samples(t *testing.T) { 12 | var tests = []struct { 13 | samples audio.Float64 14 | result float64 15 | isNaN bool 16 | }{ 17 | // Empty samples - NaN 18 | {audio.Float64{}, 0.00, true}, 19 | // Negative samples 20 | {audio.Float64{-0.10}, 0.10, false}, 21 | {audio.Float64{-0.10, -0.20}, 0.15811388300841897, false}, 22 | {audio.Float64{-0.10, -0.20, -0.30, -0.40, -0.50}, 0.33166247903554, false}, 23 | // Positive samples 24 | {audio.Float64{0.10}, 0.10, false}, 25 | {audio.Float64{0.10, 0.20}, 0.15811388300841897, false}, 26 | {audio.Float64{0.10, 0.20, 0.30, 0.40, 0.50}, 0.33166247903554, false}, 27 | // Mixed samples 28 | {audio.Float64{0.10}, 0.10, false}, 29 | {audio.Float64{0.10, -0.20}, 0.15811388300841897, false}, 30 | {audio.Float64{0.10, -0.20, 0.30, -0.40, 0.50}, 0.33166247903554, false}, 31 | } 32 | 33 | for i, test := range tests { 34 | if rms := RMSF64Samples(test.samples); rms != test.result { 35 | // If expected result is NaN, continue 36 | if math.IsNaN(rms) && test.isNaN { 37 | continue 38 | } 39 | 40 | t.Fatalf("[%02d] unexpected result: %v != %v", i, rms, test.result) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /waveform_example_test.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "image/color" 7 | "image/png" 8 | "os" 9 | ) 10 | 11 | // ExampleGenerate provides example usage of Generate, using a media file from the filesystem. 12 | // Generate is typically used for one-time, direct creation of an image.Image from 13 | // an input audio stream. 14 | func ExampleGenerate() { 15 | // Generate accepts io.Reader, so we will use a media file in the filesystem 16 | file, err := os.Open("./test/tone16bit.flac") 17 | if err != nil { 18 | fmt.Println(err) 19 | return 20 | } 21 | fmt.Println("open:", file.Name()) 22 | defer file.Close() 23 | 24 | // Directly generate waveform image from audio file, applying any number 25 | // of options functions along the way 26 | img, err := Generate(file, 27 | // Solid white background 28 | BGColorFunction(SolidColor(color.White)), 29 | // Striped red, green, and blue foreground 30 | FGColorFunction(StripeColor( 31 | color.RGBA{255, 0, 0, 255}, 32 | color.RGBA{0, 255, 0, 255}, 33 | color.RGBA{0, 0, 255, 255}, 34 | )), 35 | // Scaled 10x horizontally, 2x vertically 36 | Scale(10, 2), 37 | ) 38 | if err != nil { 39 | fmt.Println(err) 40 | return 41 | } 42 | 43 | // Encode image as PNG into buffer 44 | buf := bytes.NewBuffer(nil) 45 | if err := png.Encode(buf, img); err != nil { 46 | fmt.Println(err) 47 | return 48 | } 49 | fmt.Printf("encoded: %d bytes\nresolution: %s", buf.Len(), img.Bounds().Max) 50 | 51 | // Output: 52 | // open: ./test/tone16bit.flac 53 | // encoded: 344 bytes 54 | // resolution: (50,256) 55 | } 56 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | azul3d.org/engine v0.0.0-20180624221640-25c8eab2d474 h1:HrLWoqa15YkXa5jtwSRy7mEmmO9ZOPEFe0uMNmH+iyI= 2 | azul3d.org/engine v0.0.0-20180624221640-25c8eab2d474/go.mod h1:3y1cwzJTKvXXop+EAg+AUVfNm3bfHf3djeX+l1UBuUE= 3 | github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= 4 | github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= 5 | github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= 6 | github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= 7 | github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8= 8 | github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= 9 | github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= 10 | github.com/mewkiz/flac v1.0.6 h1:OnMwCWZPAnjDndjEzLynOZ71Y2U+/QYHoVI4JEKgKkk= 11 | github.com/mewkiz/flac v1.0.6/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= 12 | github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU= 13 | github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= 14 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 16 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 17 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | waveform [![Build Status](https://travis-ci.org/mdlayher/waveform.svg?branch=master)](https://travis-ci.org/mdlayher/waveform) [![GoDoc](http://godoc.org/github.com/mdlayher/waveform?status.svg)](http://godoc.org/github.com/mdlayher/waveform) 2 | ======== 3 | 4 | Go package capable of generating waveform images from audio streams. MIT Licensed. 5 | 6 | This library supports any audio streams which the [azul3d/engine/audio](http://azul3d.org/engine/audio) 7 | package is able to decode. At the time of writing, this includes: 8 | - WAV 9 | - FLAC 10 | 11 | An example binary called `waveform` is provided which show's the library's usage. 12 | Please see [cmd/waveform/README.md](https://github.com/mdlayher/waveform/blob/master/cmd/waveform/README.md) 13 | for details. 14 | 15 | Examples 16 | ======== 17 | 18 | Here are several example images generated using `waveform`. Enjoy! 19 | 20 | Generate a waveform image, and scale it both vertically and horizontally. 21 | 22 | ``` 23 | $ cat ~/Music/02\ -\ Peace\ Of\ Mind.flac | waveform -x 5 -y 2 > ~/waveform.png 24 | ``` 25 | 26 | ![waveform](https://cloud.githubusercontent.com/assets/1926905/4910038/6ce9f5d0-647a-11e4-8a93-ed54812d114d.png) 27 | 28 | Apply a foreground and background color, to make things more interesting. 29 | 30 | ``` 31 | cat ~/Music/02\ -\ Peace\ Of\ Mind.flac | waveform -fg=#FF3300 -bg=#0099CC -x 5 -y 2 > ~/waveform_color.png 32 | ``` 33 | 34 | ![waveform_color](https://cloud.githubusercontent.com/assets/1926905/4910043/757b0edc-647a-11e4-8ebd-73175246421d.png) 35 | 36 | Apply an alternate foreground color, draw using a stripe pattern. 37 | 38 | ``` 39 | cat ~/Music/02\ -\ Peace\ Of\ Mind.flac | waveform -fg=#FF3300 -bg=#0099CC -alt=#FF9933 -fn stripe -x 5 -y 2 > ~/waveform_stripe.png 40 | ``` 41 | 42 | ![waveform_stripe](https://cloud.githubusercontent.com/assets/1926905/4910067/a560f76a-647a-11e4-8562-c430134c1187.png) 43 | 44 | Apply an alternate foreground color, draw using a random fuzz pattern. 45 | 46 | ``` 47 | cat ~/Music/02\ -\ Peace\ Of\ Mind.flac | waveform -fg=#FF3300 -bg=#0099CC -alt=#FF9933 -fn fuzz -x 5 -y 2 > ~/waveform_fuzz.png 48 | ``` 49 | 50 | ![waveform_fuzz](https://cloud.githubusercontent.com/assets/1926905/4910076/c6aa0e70-647a-11e4-8385-754960c9f074.png) 51 | 52 | Apply a new set of colors, draw using a gradient pattern. 53 | 54 | ``` 55 | cat ~/Music/02\ -\ Peace\ Of\ Mind.flac | waveform -fg=#FF0000 -bg=#00FF00 -alt=#0000FF -fn gradient -x 5 -y 2 > ~/waveform_gradient.png 56 | ``` 57 | 58 | ![waveform_gradient](https://cloud.githubusercontent.com/assets/1926905/5416955/c5592f10-8202-11e4-943d-d86214b26b18.png) 59 | 60 | Apply a checkerboard color set, draw using a checkerboard pattern. 61 | 62 | ``` 63 | cat ~/Music/02\ -\ Peace\ Of\ Mind.flac | waveform -fg=#000000 -bg=#222222 -alt=#FFFFFF -fn checker -x 5 -y 2 > ~/waveform_checker.png 64 | ``` 65 | 66 | ![waveform_checker](https://cloud.githubusercontent.com/assets/1926905/4961769/e3280c96-66d2-11e4-8e3c-d0b843230589.png) 67 | -------------------------------------------------------------------------------- /waveform_bench_test.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "bytes" 5 | "math/rand" 6 | "testing" 7 | "time" 8 | 9 | "azul3d.org/engine/audio" 10 | ) 11 | 12 | // BenchmarkGenerateWAV checks the performance of the Generate() function with a WAV file 13 | func BenchmarkGenerateWAV(b *testing.B) { 14 | benchmarkGenerate(b, wavFile) 15 | } 16 | 17 | // BenchmarkGenerateFLAC checks the performance of the Generate() function with a FLAC file 18 | func BenchmarkGenerateFLAC(b *testing.B) { 19 | benchmarkGenerate(b, flacFile) 20 | } 21 | 22 | // BenchmarkWaveformComputeWAV checks the performance of the WaveformCompute() function with a WAV file 23 | func BenchmarkWaveformComputeWAV(b *testing.B) { 24 | benchmarkWaveformCompute(b, wavFile) 25 | } 26 | 27 | // BenchmarkWaveformComputeFLAC checks the performance of the WaveformCompute() function with a FLAC file 28 | func BenchmarkWaveformComputeFLAC(b *testing.B) { 29 | benchmarkWaveformCompute(b, flacFile) 30 | } 31 | 32 | // BenchmarkWaveformDraw60 checks the performance of the WaveformDraw() function 33 | // with approximately 60 seconds of computed values 34 | func BenchmarkWaveformDraw60(b *testing.B) { 35 | benchmarkWaveformDraw(b, 60) 36 | } 37 | 38 | // BenchmarkWaveformDraw120 checks the performance of the WaveformDraw() function 39 | // with approximately 120 seconds of computed values 40 | func BenchmarkWaveformDraw120(b *testing.B) { 41 | benchmarkWaveformDraw(b, 120) 42 | } 43 | 44 | // BenchmarkWaveformDraw240 checks the performance of the WaveformDraw() function 45 | // with approximately 240 seconds of computed values 46 | func BenchmarkWaveformDraw240(b *testing.B) { 47 | benchmarkWaveformDraw(b, 240) 48 | } 49 | 50 | // BenchmarkWaveformDraw480 checks the performance of the WaveformDraw() function 51 | // with approximately 480 seconds of computed values 52 | func BenchmarkWaveformDraw480(b *testing.B) { 53 | benchmarkWaveformDraw(b, 480) 54 | } 55 | 56 | // BenchmarkWaveformDraw960 checks the performance of the WaveformDraw() function 57 | // with approximately 960 seconds of computed values 58 | func BenchmarkWaveformDraw960(b *testing.B) { 59 | benchmarkWaveformDraw(b, 960) 60 | } 61 | 62 | // BenchmarkRMSF64Samples22050 checks the performance of the RMSF64Samples() function 63 | // with 22050 samples 64 | func BenchmarkRMSF64Samples22050(b *testing.B) { 65 | benchmarkRMSF64Samples(b, 22050) 66 | } 67 | 68 | // BenchmarkRMSF64Samples44100 checks the performance of the RMSF64Samples() function 69 | // with 44100 samples 70 | func BenchmarkRMSF64Samples44100(b *testing.B) { 71 | benchmarkRMSF64Samples(b, 44100) 72 | } 73 | 74 | // BenchmarkRMSF64Samples88200 checks the performance of the RMSF64Samples() function 75 | // with 88200 samples 76 | func BenchmarkRMSF64Samples88200(b *testing.B) { 77 | benchmarkRMSF64Samples(b, 88200) 78 | } 79 | 80 | // BenchmarkRMSF64Samples176400 checks the performance of the RMSF64Samples() function 81 | // with 176400 samples 82 | func BenchmarkRMSF64Samples176400(b *testing.B) { 83 | benchmarkRMSF64Samples(b, 176400) 84 | } 85 | 86 | // benchmarkGenerate contains common logic for benchmarking Generate 87 | func benchmarkGenerate(b *testing.B, data []byte) { 88 | for i := 0; i < b.N; i++ { 89 | Generate(bytes.NewReader(data)) 90 | } 91 | } 92 | 93 | // benchmarkWaveformCompute contains common logic for benchmarking Waveform.Compute 94 | func benchmarkWaveformCompute(b *testing.B, data []byte) { 95 | for i := 0; i < b.N; i++ { 96 | w, err := New(bytes.NewReader(data)) 97 | if err != nil { 98 | panic(err) 99 | } 100 | 101 | w.Compute() 102 | } 103 | } 104 | 105 | // benchmarkWaveformDraw contains common logic for benchmarking Waveform.Draw 106 | func benchmarkWaveformDraw(b *testing.B, count int) { 107 | values := make([]float64, count) 108 | for i := 0; i < b.N; i++ { 109 | w, err := New(nil) 110 | if err != nil { 111 | panic(err) 112 | } 113 | 114 | w.Draw(values) 115 | } 116 | } 117 | 118 | // benchmarkRMSF64Samples contains common logic for benchmarking RMSF64Samples 119 | func benchmarkRMSF64Samples(b *testing.B, count int) { 120 | // Generate slice of samples 121 | rand.Seed(time.Now().UnixNano()) 122 | var samples audio.Float64 123 | for i := 0; i < count; i++ { 124 | samples = append(samples, rand.Float64()) 125 | } 126 | 127 | // Reset timer and start benchmark 128 | b.ResetTimer() 129 | for i := 0; i < b.N; i++ { 130 | RMSF64Samples(samples) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /colorfunc.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "image/color" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // ColorFunc is a function which accepts a variety of values which can be used 10 | // to customize an output image. These values include the current computed sample 11 | // count (n), the current X coordinate (x), the current Y coordinate (y), and the 12 | // maximum computed values for each of these. (maxN, maxX, maxY) 13 | // 14 | // A ColorFunc is applied during each image drawing iteration, and will 15 | // return the appropriate color which should be drawn at the specified value 16 | // for n, x, and y; possibly taking into account their maximum values. 17 | type ColorFunc func(n int, x int, y int, maxN int, maxX int, maxY int) color.Color 18 | 19 | // CheckerColor generates a ColorFunc which produces a checkerboard pattern, 20 | // using the two input colors. Each square is drawn to the size specified by 21 | // the size parameter. 22 | func CheckerColor(colorA color.Color, colorB color.Color, size uint) ColorFunc { 23 | return func(n int, x int, y int, maxN int, maxX int, maxY int) color.Color { 24 | if ((uint(x)/size)+(uint(y)/size))%2 == 0 { 25 | return colorA 26 | } 27 | 28 | return colorB 29 | } 30 | } 31 | 32 | // FuzzColor generates a ColorFunc which applies a random color on each call, 33 | // selected from an input, variadic slice of colors. This can be used to create 34 | // a random fuzz or "static" effect in the resulting waveform image. 35 | func FuzzColor(colors ...color.Color) ColorFunc { 36 | // Filter any nil values 37 | colors = filterNilColors(colors) 38 | 39 | // Seed RNG 40 | rand.Seed(time.Now().UnixNano()) 41 | 42 | // Select a color at random on each call 43 | return func(n int, x int, y int, maxN int, maxX int, maxY int) color.Color { 44 | return colors[rand.Intn(len(colors))] 45 | } 46 | } 47 | 48 | // GradientColor generates a ColorFunc which produces a color gradient between two 49 | // RGBA input colors. The gradient attempts to gradually reduce the distance between 50 | // two colors, creating a sweeping color change effect in the resulting waveform 51 | // image. 52 | func GradientColor(start color.RGBA, end color.RGBA) ColorFunc { 53 | // Float equivalents of color values 54 | startFR, endFR := float64(start.R), float64(end.R) 55 | startFG, endFG := float64(start.G), float64(end.G) 56 | startFB, endFB := float64(start.B), float64(end.B) 57 | 58 | // Values used for RGBA and percentage 59 | var r, g, b, p float64 60 | return func(n int, x int, y int, maxN int, maxX int, maxY int) color.Color { 61 | // Calculate percentage across waveform image 62 | p = float64((float64(n) / float64(maxN)) * 100) 63 | 64 | // Calculate new values for RGB using gradient algorithm 65 | // Thanks: http://stackoverflow.com/questions/27532/generating-gradients-programmatically 66 | r = (endFR * p) + (startFR * (1 - p)) 67 | g = (endFG * p) + (startFG * (1 - p)) 68 | b = (endFB * p) + (startFB * (1 - p)) 69 | 70 | // Correct overflow when moving from lighter to darker gradients 71 | if start.R > end.R && r > -255.00 { 72 | r = -255.00 73 | } 74 | if start.G > end.G && g > -255.00 { 75 | g = -255.00 76 | } 77 | if start.B > end.B && b > -255.00 { 78 | b = -255.00 79 | } 80 | 81 | // Generate output color 82 | return &color.RGBA{ 83 | R: uint8(r / 100), 84 | G: uint8(g / 100), 85 | B: uint8(b / 100), 86 | A: 255, 87 | } 88 | } 89 | } 90 | 91 | // SolidColor generates a ColorFunc which simply returns the input color 92 | // as the color which should be drawn at all coordinates. 93 | // 94 | // This is the default behavior of the waveform package. 95 | func SolidColor(inColor color.Color) ColorFunc { 96 | return func(n int, x int, y int, maxN int, maxX int, maxY int) color.Color { 97 | return inColor 98 | } 99 | } 100 | 101 | // StripeColor generates a ColorFunc which applies one color from the input, 102 | // variadic slice at each computed value. Each color is used in order, and 103 | // the rotation will repeat until the image is complete. This creates a stripe 104 | // effect in the resulting waveform image. 105 | func StripeColor(colors ...color.Color) ColorFunc { 106 | // Filter any nil values 107 | colors = filterNilColors(colors) 108 | 109 | var lastN int 110 | return func(n int, x int, y int, maxN int, maxX int, maxY int) color.Color { 111 | // For each new n value, use the next color in the slice 112 | if n > lastN { 113 | lastN = n 114 | } 115 | 116 | return colors[lastN%len(colors)] 117 | } 118 | } 119 | 120 | // filterNilColors strips any nil color.Color values from the input slice. 121 | func filterNilColors(colors []color.Color) []color.Color { 122 | var cleanColors []color.Color 123 | for _, c := range colors { 124 | if c != nil { 125 | cleanColors = append(cleanColors, c) 126 | } 127 | } 128 | 129 | return cleanColors 130 | } 131 | -------------------------------------------------------------------------------- /cmd/waveform/waveform.go: -------------------------------------------------------------------------------- 1 | // Command waveform is a simple utility which reads an audio file from stdin, 2 | // processes it into a waveform image using input flags, and writes a PNG image 3 | // of the generated waveform to stdout. 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "image/color" 10 | "image/png" 11 | "log" 12 | "os" 13 | "strconv" 14 | 15 | "github.com/mdlayher/waveform" 16 | ) 17 | 18 | const ( 19 | // app is the name of this application 20 | app = "waveform" 21 | 22 | // Names of available color functions 23 | fnChecker = "checker" 24 | fnFuzz = "fuzz" 25 | fnGradient = "gradient" 26 | fnSolid = "solid" 27 | fnStripe = "stripe" 28 | ) 29 | 30 | var ( 31 | // strBGColor is the hex color value used to color the background of the waveform image 32 | strBGColor = flag.String("bg", "#FFFFFF", "hex background color of output waveform image") 33 | 34 | // strFGColor is the hex color value used to color the foreground of the waveform image 35 | strFGColor = flag.String("fg", "#000000", "hex foreground color of output waveform image") 36 | 37 | // strAltColor is the hex color value used to set the alternate color of the waveform image 38 | strAltColor = flag.String("alt", "", "hex alternate color of output waveform image") 39 | 40 | // resolution is the number of times audio is read and the waveform is drawn, 41 | // per second of audio 42 | resolution = flag.Uint("resolution", 1, "number of times audio is read and drawn per second of audio") 43 | 44 | // scaleX is the scaling factor for the output waveform file's X-axis 45 | scaleX = flag.Uint("x", 1, "scaling factor for image X-axis") 46 | 47 | // scaleY is the scaling factor for the output waveform file's Y-axis 48 | scaleY = flag.Uint("y", 1, "scaling factor for image Y-axis") 49 | 50 | // sharpness is the factor used to add curvature to a scaled image, preventing 51 | // "blocky" images at higher scaling 52 | sharpness = flag.Uint("sharpness", 1, "sharpening factor used to add curvature to a scaled image") 53 | 54 | // strFn is an identifier which selects the ColorFunc used to color the waveform image 55 | strFn = flag.String("fn", fnSolid, "function used to color output waveform image "+fnOptions) 56 | ) 57 | 58 | // fnOptions is the help string which lists available options 59 | var fnOptions = fmt.Sprintf("[options: %s, %s, %s, %s, %s]", fnChecker, fnFuzz, fnGradient, fnSolid, fnStripe) 60 | 61 | func main() { 62 | // Parse flags 63 | flag.Parse() 64 | 65 | // Move all logging output to stderr, as output image will occupy 66 | // the stdout stream 67 | log.SetOutput(os.Stderr) 68 | log.SetPrefix(app + ": ") 69 | 70 | // Create image background color from input hex color string, or default 71 | // to black if invalid 72 | colorR, colorG, colorB := hexToRGB(*strBGColor) 73 | bgColor := color.RGBA{colorR, colorG, colorB, 255} 74 | 75 | // Create image foreground color from input hex color string, or default 76 | // to black if invalid 77 | colorR, colorG, colorB = hexToRGB(*strFGColor) 78 | fgColor := color.RGBA{colorR, colorG, colorB, 255} 79 | 80 | // Create image alternate color from input hex color string, or default 81 | // to foreground color if empty 82 | altColor := fgColor 83 | if *strAltColor != "" { 84 | colorR, colorG, colorB = hexToRGB(*strAltColor) 85 | altColor = color.RGBA{colorR, colorG, colorB, 255} 86 | } 87 | 88 | // Set of available functions 89 | fnSet := map[string]waveform.ColorFunc{ 90 | fnChecker: waveform.CheckerColor(fgColor, altColor, 10), 91 | fnFuzz: waveform.FuzzColor(fgColor, altColor), 92 | fnGradient: waveform.GradientColor(fgColor, altColor), 93 | fnSolid: waveform.SolidColor(fgColor), 94 | fnStripe: waveform.StripeColor(fgColor, altColor), 95 | } 96 | 97 | // Validate user-selected function 98 | colorFn, ok := fnSet[*strFn] 99 | if !ok { 100 | log.Fatalf("unknown function: %q %s", *strFn, fnOptions) 101 | } 102 | 103 | // Generate a waveform image from stdin, using values passed from 104 | // flags as options 105 | img, err := waveform.Generate(os.Stdin, 106 | waveform.BGColorFunction(waveform.SolidColor(bgColor)), 107 | waveform.FGColorFunction(colorFn), 108 | waveform.Resolution(*resolution), 109 | waveform.Scale(*scaleX, *scaleY), 110 | waveform.ScaleClipping(), 111 | waveform.Sharpness(*sharpness), 112 | ) 113 | if err != nil { 114 | // Set of known errors 115 | knownErr := map[error]struct{}{ 116 | waveform.ErrFormat: struct{}{}, 117 | waveform.ErrInvalidData: struct{}{}, 118 | waveform.ErrUnexpectedEOS: struct{}{}, 119 | } 120 | 121 | // On known error, fatal log 122 | if _, ok := knownErr[err]; ok { 123 | log.Fatal(err) 124 | } 125 | 126 | // Unknown errors, panic 127 | panic(err) 128 | } 129 | 130 | // Encode results as PNG to stdout 131 | if err := png.Encode(os.Stdout, img); err != nil { 132 | panic(err) 133 | } 134 | } 135 | 136 | // hexToRGB converts a hex string to a RGB triple. 137 | // Credit: https://code.google.com/p/gorilla/source/browse/color/hex.go?r=ef489f63418265a7249b1d53bdc358b09a4a2ea0 138 | func hexToRGB(h string) (uint8, uint8, uint8) { 139 | if len(h) > 0 && h[0] == '#' { 140 | h = h[1:] 141 | } 142 | if len(h) == 3 { 143 | h = h[:1] + h[:1] + h[1:2] + h[1:2] + h[2:] + h[2:] 144 | } 145 | if len(h) == 6 { 146 | if rgb, err := strconv.ParseUint(string(h), 16, 32); err == nil { 147 | return uint8(rgb >> 16), uint8((rgb >> 8) & 0xFF), uint8(rgb & 0xFF) 148 | } 149 | } 150 | return 0, 0, 0 151 | } 152 | -------------------------------------------------------------------------------- /colorfunc_test.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | // Named colors for easy testing 9 | var ( 10 | black = color.RGBA{0, 0, 0, 255} 11 | white = color.RGBA{255, 255, 255, 255} 12 | red = color.RGBA{255, 0, 0, 255} 13 | green = color.RGBA{0, 255, 0, 255} 14 | blue = color.RGBA{0, 0, 255, 255} 15 | ) 16 | 17 | // TestCheckerColorOneColor verifies that CheckerColor produces only the single 18 | // color used in its input. 19 | func TestCheckerColorOneColor(t *testing.T) { 20 | testCheckerColor(t, black, black) 21 | } 22 | 23 | // TestCheckerColorTwoColors verifies that CheckerColor produces only colors 24 | // which are used in its input. 25 | func TestCheckerColorTwoColors(t *testing.T) { 26 | testCheckerColor(t, black, white) 27 | } 28 | 29 | // TestFuzzColorOneColor verifies that FuzzColor produces only the single 30 | // color used in its input. 31 | func TestFuzzColorOneColor(t *testing.T) { 32 | testFuzzColor(t, []color.Color{black}) 33 | } 34 | 35 | // TestFuzzColorMultipleColors verifies that FuzzColor produces only colors 36 | // which are used in its input. 37 | func TestFuzzColorMultipleColors(t *testing.T) { 38 | testFuzzColor(t, []color.Color{black, white, red, green, blue}) 39 | } 40 | 41 | // TestGradientColorOneColor verifies that GradientColor produces only the single 42 | // color used in its input. 43 | func TestGradientColorOneColor(t *testing.T) { 44 | testGradientColor(t, black, black) 45 | } 46 | 47 | // TestGradientColorTwoColors verifies that GradientColor produces a correct 48 | // gradient between two colors. 49 | func TestGradientColorTwoColors(t *testing.T) { 50 | testGradientColor(t, black, white) 51 | } 52 | 53 | // TestSolidColor verifies that SolidColor always returns the same input 54 | // color, for all input values. 55 | func TestSolidColor(t *testing.T) { 56 | colors := []color.Color{ 57 | black, 58 | white, 59 | } 60 | 61 | for i, c := range colors { 62 | fn := SolidColor(c) 63 | if out := fn(i, i, i, i, i, i); out != c { 64 | t.Fatalf("unexpected SolidColor color: %v != %v", out, c) 65 | } 66 | } 67 | } 68 | 69 | // TestStripeColorOneColor verifies that StripeColor produces a correct 70 | // color sequence with a single input color. 71 | func TestStripeColorOneColor(t *testing.T) { 72 | testStripeColor(t, []color.Color{black}, []color.Color{ 73 | black, black, black, black, 74 | }) 75 | } 76 | 77 | // TestStripeColorMultipleColors verifies that StripeColor produces a correct 78 | // color sequence with multiple input colors. 79 | func TestStripeColorMultipleColors(t *testing.T) { 80 | testStripeColor(t, []color.Color{ 81 | black, white, white, red, green, green, green, blue, 82 | }, []color.Color{ 83 | black, white, white, red, green, green, green, blue, 84 | black, white, white, red, green, green, green, blue, 85 | }) 86 | } 87 | 88 | // testCheckerColor is a test helper which aids in testing the CheckerColor function. 89 | func testCheckerColor(t *testing.T, colorA color.Color, colorB color.Color) { 90 | // Predefined values for test 91 | const maxX, maxY, size = 1000, 1000, 10 92 | 93 | // Generate checker function with input values 94 | fn := CheckerColor(colorA, colorB, size) 95 | 96 | // Iterate all coordinates and check color at each 97 | for x := 0; x < maxX; x++ { 98 | for y := 0; y < maxY; y++ { 99 | // Check color at specified coordinate 100 | c := fn(0, x, y, 0, maxX, maxY) 101 | 102 | // Apply checker algorithm to determine if color A or B should be used 103 | if ((uint(x)/size)+(uint(y)/size))%2 == 0 { 104 | if c != colorA { 105 | t.Fatalf("unexpected color: %v != %v", c, colorA) 106 | } 107 | } else { 108 | if c != colorB { 109 | t.Fatalf("unexpected color: %v != %v", c, colorB) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | // testFuzzColor is a test helper which aids in testing the FuzzColor function. 117 | func testFuzzColor(t *testing.T, in []color.Color) { 118 | // Make a set of colors from input slice 119 | set := make(map[color.RGBA]struct{}) 120 | for _, c := range in { 121 | set[c.(color.RGBA)] = struct{}{} 122 | } 123 | 124 | // Validate that FuzzColor only produces colors which are present in 125 | // the input slice. 126 | fn := FuzzColor(in...) 127 | for i := 0; i < 10000; i++ { 128 | if out, ok := set[fn(i, i, i, i, i, i).(color.RGBA)]; !ok { 129 | t.Fatalf("color not in set: %v", out) 130 | } 131 | } 132 | } 133 | 134 | // testGradientColor is a test helper which aids in testing the GradientColor 135 | // function. 136 | func testGradientColor(t *testing.T, start color.RGBA, end color.RGBA) { 137 | const maxN = 100 138 | 139 | // Generate function with defined values 140 | fn := GradientColor(start, end) 141 | 142 | // Check edges 143 | for i, n := range []int{0, maxN} { 144 | // Get color at point, get RGBA equivalent 145 | c := fn(n, 0, 0, maxN, 0, 0) 146 | r, g, b, _ := c.RGBA() 147 | 148 | // First iteration, use start; second, use end 149 | var testColor color.RGBA 150 | if i == 0 { 151 | testColor = start 152 | } else { 153 | testColor = end 154 | } 155 | 156 | // Compare values to ensure correctness 157 | if testColor.R != uint8(r) || testColor.G != uint8(g) || testColor.B != uint8(b) { 158 | t.Fatalf("unexpected color at %d%%: %v != %v", n, c, testColor) 159 | } 160 | } 161 | } 162 | 163 | // testStripeColor is a test helper which aids in testing the StripeColor function. 164 | func testStripeColor(t *testing.T, in []color.Color, out []color.Color) { 165 | // Validate that StripeColor produces expected output at each index 166 | fn := StripeColor(in...) 167 | for i := 0; i < len(out); i++ { 168 | if c := fn(i, 0, 0, 0, 0, 0); c != out[i] { 169 | t.Fatalf("[%02d] unexpected output color: %v != %v", i, c, out[i]) 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /waveform_test.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "testing" 9 | ) 10 | 11 | var ( 12 | // Read in test files 13 | wavFile = func() []byte { 14 | file, err := ioutil.ReadFile("./test/tone16bit.wav") 15 | if err != nil { 16 | log.Fatalf("could not open test WAV: %v", err) 17 | } 18 | 19 | return file 20 | }() 21 | flacFile = func() []byte { 22 | file, err := ioutil.ReadFile("./test/tone16bit.flac") 23 | if err != nil { 24 | log.Fatalf("could not open test FLAC: %v", err) 25 | } 26 | 27 | return file 28 | }() 29 | mp3File = func() []byte { 30 | file, err := ioutil.ReadFile("./test/tone16bit.mp3") 31 | if err != nil { 32 | log.Fatalf("could not open test MP3: %v", err) 33 | } 34 | 35 | return file 36 | }() 37 | oggVorbisFile = func() []byte { 38 | file, err := ioutil.ReadFile("./test/tone16bit.ogg") 39 | if err != nil { 40 | log.Fatalf("could not open test Ogg Vorbis: %v", err) 41 | } 42 | 43 | return file 44 | }() 45 | ) 46 | 47 | // TestWaveformComputeWAVOK verifies that the Waveform.Compute method produces 48 | // appropriate computed samples and error for an input audio stream. 49 | // The input stream is in WAV format, and no errors should occur. 50 | func TestWaveformComputeWAVOK(t *testing.T) { 51 | testWaveformCompute(t, bytes.NewReader(wavFile), nil, 52 | []float64{ 53 | 0.7071166200538482, 54 | 0.7071166444294603, 55 | 0.7071166239921965, 56 | 0.7071165471800284, 57 | 0.7071166825227931, 58 | 0.7071166825227931, 59 | }, 60 | nil, 61 | ) 62 | } 63 | 64 | // TestWaveformComputeWAVErrInvalidData verifies that the Waveform.Compute method produces 65 | // appropriate computed samples and error for an input audio stream. 66 | // The input stream is in WAV format, but contains invalid data. 67 | func TestWaveformComputeWAVErrInvalidData(t *testing.T) { 68 | testWaveformCompute(t, bytes.NewReader([]byte{'R', 'I', 'F', 'F', 'W', 'A', 'V', '3'}), ErrInvalidData, nil, nil) 69 | } 70 | 71 | // TestWaveformComputeWAVEOF verifies that the Waveform.Compute method produces 72 | // appropriate computed samples and error for an input audio stream. 73 | // The input stream is in WAV format, but reaches EOF during decoding. 74 | func TestWaveformComputeWAVEOF(t *testing.T) { 75 | testWaveformCompute(t, bytes.NewReader([]byte{'R', 'I', 'F', 'F'}), io.EOF, nil, nil) 76 | } 77 | 78 | // TestWaveformComputeFLACOK verifies that the Waveform.Compute method produces 79 | // appropriate computed samples and error for an input audio stream. 80 | // The input stream is in FLAC format, and no errors should occur. 81 | func TestWaveformComputeFLACOK(t *testing.T) { 82 | testWaveformCompute(t, bytes.NewReader(flacFile), nil, 83 | []float64{ 84 | 0.7071166200538482, 85 | 0.7071166444294603, 86 | 0.7071166239921965, 87 | 0.7071165471800284, 88 | 0.7071166825227931, 89 | }, 90 | nil, 91 | ) 92 | } 93 | 94 | // TestWaveformComputeFLACErrInvalidData verifies that the Waveform.Compute method produces 95 | // appropriate computed samples and error for an input audio stream. 96 | // The input stream is in FLAC format, but contains invalid data. 97 | func TestWaveformComputeFLACErrInvalidData(t *testing.T) { 98 | testWaveformCompute(t, bytes.NewReader([]byte{'f', 'L', 'a', 'C'}), ErrInvalidData, nil, nil) 99 | } 100 | 101 | // TestWaveformComputeMP3ErrFormat verifies that the Waveform.Compute method produces 102 | // appropriate computed samples and error for an input audio stream. 103 | // The input stream is in MP3 format, and should produce an unsupported format error. 104 | func TestWaveformComputeMP3ErrFormat(t *testing.T) { 105 | testWaveformCompute(t, bytes.NewReader(mp3File), ErrFormat, nil, nil) 106 | } 107 | 108 | // TestWaveformComputeOggVorbisErrFormat verifies that the Waveform.Compute method produces 109 | // appropriate computed samples and error for an input audio stream. 110 | // The input stream is in Ogg Vorbis format, and should produce an unsupported format error. 111 | func TestWaveformComputeOggVorbisErrFormat(t *testing.T) { 112 | testWaveformCompute(t, bytes.NewReader(oggVorbisFile), ErrFormat, nil, nil) 113 | } 114 | 115 | // TestWaveformComputeSampleFuncFunctionNil verifies that the Waveform.Compute method returns an error 116 | // if a nil SampleReduceFunc member is set. 117 | func TestWaveformComputeSampleFuncFunctionNil(t *testing.T) { 118 | if _, err := new(Waveform).Compute(); err != errSampleFunctionNil { 119 | t.Fatalf("unexpected Compute error: %v != %v", err, errSampleFunctionNil) 120 | } 121 | } 122 | 123 | // TestWaveformComputeResolutionZero verifies that the Waveform.Compute method returns an error 124 | // if the resolution member is 0. 125 | func TestWaveformComputeResolutionZero(t *testing.T) { 126 | w := &Waveform{ 127 | sampleFn: RMSF64Samples, 128 | } 129 | if _, err := w.Compute(); err != errResolutionZero { 130 | t.Fatalf("unexpected Compute error: %v != %v", err, errResolutionZero) 131 | } 132 | } 133 | 134 | // testWaveformCompute is a test helper which verifies that generating a Waveform 135 | // from an input io.Reader, applying the appropriate OptionsFunc, and calling its 136 | // Compute method, will produce the appropriate computed values and error. 137 | func testWaveformCompute(t *testing.T, r io.Reader, err error, values []float64, fn []OptionsFunc) { 138 | // Generate new Waveform, apply any functions 139 | w, wErr := New(r, fn...) 140 | if wErr != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | // Compute values from waveform 145 | computed, cErr := w.Compute() 146 | if cErr != err { 147 | t.Fatalf("unexpected Compute error: %v != %v", cErr, err) 148 | } 149 | 150 | // Ensure values slices match in length 151 | if len(values) != len(computed) { 152 | t.Fatalf("unexpected Compute values length: %v != %v [%v != %v]", len(values), len(computed), values, computed) 153 | } 154 | 155 | // Iterate all values and check for equality 156 | for i := range values { 157 | if values[i] != computed[i] { 158 | t.Fatalf("unexpected Compute value at index %d: %v != %v", i, values[i], computed[i]) 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "testing" 7 | ) 8 | 9 | // TestOptionsError verifies that the format of OptionsError.Error does 10 | // not change. 11 | func TestOptionsError(t *testing.T) { 12 | var tests = []struct { 13 | option string 14 | reason string 15 | }{ 16 | {"foo", "bar"}, 17 | {"baz", "qux"}, 18 | {"one", "two"}, 19 | } 20 | 21 | for _, test := range tests { 22 | // Generate options error 23 | opErr := &OptionsError{ 24 | Option: test.option, 25 | Reason: test.reason, 26 | } 27 | 28 | // Verify correct format 29 | if opErr.Error() != fmt.Sprintf("%s: %s", test.option, test.reason) { 30 | t.Fatalf("unexpected Error string: %v", opErr.Error()) 31 | } 32 | } 33 | } 34 | 35 | // TestOptionBGColorFunctionOK verifies that BGColorFunction returns no error 36 | // with acceptable input. 37 | func TestOptionBGColorFunctionOK(t *testing.T) { 38 | testWaveformOptionFunc(t, BGColorFunction(SolidColor(color.Black)), nil) 39 | } 40 | 41 | // TestOptionBGColorFunctionNil verifies that BGColorFunction does not accept 42 | // a nil ColorReduceFunc. 43 | func TestOptionBGColorFunctionNil(t *testing.T) { 44 | testWaveformOptionFunc(t, BGColorFunction(nil), errBGColorFunctionNil) 45 | } 46 | 47 | // TestOptionFGColorFunctionOK verifies that FGColorFunction returns no error 48 | // with acceptable input. 49 | func TestOptionFGColorFunctionOK(t *testing.T) { 50 | testWaveformOptionFunc(t, FGColorFunction(SolidColor(color.Black)), nil) 51 | } 52 | 53 | // TestOptionFGColorFunctionNil verifies that FGColorFunction does not accept 54 | // a nil ColorReduceFunc. 55 | func TestOptionFGColorFunctionNil(t *testing.T) { 56 | testWaveformOptionFunc(t, FGColorFunction(nil), errFGColorFunctionNil) 57 | } 58 | 59 | // TestOptionSampleFunctionOK verifies that SampleFunction returns no error 60 | // with acceptable input. 61 | func TestOptionSampleFunctionOK(t *testing.T) { 62 | testWaveformOptionFunc(t, SampleFunction(RMSF64Samples), nil) 63 | } 64 | 65 | // TestOptionSampleFunctionNil verifies that SampleFunction does not accept 66 | // a nil SampleReduceFunc. 67 | func TestOptionSampleFunctionNil(t *testing.T) { 68 | testWaveformOptionFunc(t, SampleFunction(nil), errSampleFunctionNil) 69 | } 70 | 71 | // TestOptionResolutionOK verifies that Resolution returns no error with acceptable input. 72 | func TestOptionResolutionOK(t *testing.T) { 73 | testWaveformOptionFunc(t, Resolution(1), nil) 74 | } 75 | 76 | // TestOptionResolutionZero verifies that Resolution does not accept integer 0. 77 | func TestOptionResolutionZero(t *testing.T) { 78 | testWaveformOptionFunc(t, Resolution(0), errResolutionZero) 79 | } 80 | 81 | // TestOptionScaleOK verifies that Scale returns no error with acceptable input. 82 | func TestOptionScaleOK(t *testing.T) { 83 | testWaveformOptionFunc(t, Scale(1, 1), nil) 84 | } 85 | 86 | // TestOptionScaleXZero verifies that Scale does not accept an X value integer 0. 87 | func TestOptionScaleXZero(t *testing.T) { 88 | testWaveformOptionFunc(t, Scale(0, 1), errScaleXZero) 89 | } 90 | 91 | // TestOptionScaleYZero verifies that Scale does not accept an Y value integer 0. 92 | func TestOptionScaleYZero(t *testing.T) { 93 | testWaveformOptionFunc(t, Scale(1, 0), errScaleYZero) 94 | } 95 | 96 | // TestOptionScaleClippingOK verifies that ScaleClipping returns no error. 97 | func TestOptionScaleClippingOK(t *testing.T) { 98 | testWaveformOptionFunc(t, ScaleClipping(), nil) 99 | } 100 | 101 | // TestOptionSharpnessOK verifies that Sharpness returns no error. 102 | func TestOptionSharpnessOK(t *testing.T) { 103 | testWaveformOptionFunc(t, Sharpness(0), nil) 104 | } 105 | 106 | // TestWaveformSetOptionsNil verifies that Waveform.SetOptions ignores any 107 | // nil OptionsFunc arguments. 108 | func TestWaveformSetOptionsNil(t *testing.T) { 109 | testWaveformOptionFunc(t, nil, nil) 110 | } 111 | 112 | // TestWaveformSetBGColorFunction verifies that the Waveform.SetBGColorFunction 113 | // method properly modifies struct members. 114 | func TestWaveformSetBGColorFunction(t *testing.T) { 115 | // Generate empty Waveform, apply parameters 116 | w := &Waveform{} 117 | if err := w.SetBGColorFunction(SolidColor(color.Black)); err != nil { 118 | t.Fatal(err) 119 | } 120 | 121 | // Validate that struct members are set properly 122 | if w.bgColorFn == nil { 123 | t.Fatalf("SetBGColorFunction failed, nil function member") 124 | } 125 | } 126 | 127 | // TestWaveformSetFGColorFunction verifies that the Waveform.SetFGColorFunction 128 | // method properly modifies struct members. 129 | func TestWaveformSetFGColorFunction(t *testing.T) { 130 | // Generate empty Waveform, apply parameters 131 | w := &Waveform{} 132 | if err := w.SetFGColorFunction(SolidColor(color.Black)); err != nil { 133 | t.Fatal(err) 134 | } 135 | 136 | // Validate that struct members are set properly 137 | if w.fgColorFn == nil { 138 | t.Fatalf("SetFGColorFunction failed, nil function member") 139 | } 140 | } 141 | 142 | // TestWaveformSetSampleFunction verifies that the Waveform.SetSampleFunction 143 | // method properly modifies struct members. 144 | func TestWaveformSetSampleFunction(t *testing.T) { 145 | // Generate empty Waveform, apply parameters 146 | w := &Waveform{} 147 | if err := w.SetSampleFunction(RMSF64Samples); err != nil { 148 | t.Fatal(err) 149 | } 150 | 151 | // Validate that struct members are set properly 152 | if w.sampleFn == nil { 153 | t.Fatalf("SetSampleFunction failed, nil function member") 154 | } 155 | } 156 | 157 | // TestWaveformSetResolution verifies that the Waveform.SetResolution method properly 158 | // modifies struct members. 159 | func TestWaveformSetResolution(t *testing.T) { 160 | // Predefined test values 161 | res := uint(1) 162 | 163 | // Generate empty Waveform, apply parameters 164 | w := &Waveform{} 165 | if err := w.SetResolution(res); err != nil { 166 | t.Fatal(err) 167 | } 168 | 169 | // Validate that struct members are set properly 170 | if w.resolution != res { 171 | t.Fatalf("unexpected resolution: %v != %v", w.resolution, res) 172 | } 173 | } 174 | 175 | // TestWaveformSetScale verifies that the Waveform.SetScale method properly 176 | // modifies struct members. 177 | func TestWaveformSetScale(t *testing.T) { 178 | // Predefined test values 179 | x := uint(1) 180 | y := uint(1) 181 | 182 | // Generate empty Waveform, apply parameters 183 | w := &Waveform{} 184 | if err := w.SetScale(x, y); err != nil { 185 | t.Fatal(err) 186 | } 187 | 188 | // Validate that struct members are set properly 189 | if w.scaleX != x { 190 | t.Fatalf("unexpected scale X: %v != %v", w.scaleX, x) 191 | } 192 | if w.scaleY != y { 193 | t.Fatalf("unexpected scale Y: %v != %v", w.scaleY, y) 194 | } 195 | } 196 | 197 | // TestWaveformSetScaleClipping verifies that the Waveform.SetScaleClipping method properly 198 | // modifies struct members. 199 | func TestWaveformSetScaleClipping(t *testing.T) { 200 | // Generate empty Waveform, apply function 201 | w := &Waveform{} 202 | if err := w.SetScaleClipping(); err != nil { 203 | t.Fatal(err) 204 | } 205 | 206 | // Validate that struct members are set properly 207 | if !w.scaleClipping { 208 | t.Fatalf("SetScaleClipping failed, false scaleClipping member") 209 | } 210 | } 211 | 212 | // TestWaveformSetSharpness verifies that the Waveform.SetSharpness method properly 213 | // modifies struct members. 214 | func TestWaveformSetSharpness(t *testing.T) { 215 | // Predefined test values 216 | sharpness := uint(1) 217 | 218 | // Generate empty Waveform, apply parameters 219 | w := &Waveform{} 220 | if err := w.SetSharpness(sharpness); err != nil { 221 | t.Fatal(err) 222 | } 223 | 224 | // Validate that struct members are set properly 225 | if w.sharpness != sharpness { 226 | t.Fatalf("unexpected sharpness: %v != %v", w.sharpness, sharpness) 227 | } 228 | } 229 | 230 | // testWaveformOptionFunc is a test helper which verifies that applying the 231 | // input OptionsFunc to a new Waveform struct generates the appropriate 232 | // error output. 233 | func testWaveformOptionFunc(t *testing.T, fn OptionsFunc, err error) { 234 | if _, wErr := New(nil, fn); wErr != err { 235 | t.Fatalf("unexpected error: %v != %v", wErr, err) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package waveform 2 | 3 | import "fmt" 4 | 5 | var ( 6 | // errBGColorFunctionNil is returned when a nil ColorFunc is used in 7 | // a call to BGColorFunction. 8 | errBGColorFunctionNil = &OptionsError{ 9 | Option: "bgColorFunction", 10 | Reason: "function cannot be nil", 11 | } 12 | 13 | // errFGColorFunctionNil is returned when a nil ColorFunc is used in 14 | // a call to FGColorFunction. 15 | errFGColorFunctionNil = &OptionsError{ 16 | Option: "fgColorFunction", 17 | Reason: "function cannot be nil", 18 | } 19 | 20 | // errSampleFunctionNil is returned when a nil SampleReduceFunc is used in 21 | // a call to SampleFunc. 22 | errSampleFunctionNil = &OptionsError{ 23 | Option: "sampleFunction", 24 | Reason: "function cannot be nil", 25 | } 26 | 27 | // errResolutionZero is returned when integer 0 is used in a call 28 | // to Resolution. 29 | errResolutionZero = &OptionsError{ 30 | Option: "resolution", 31 | Reason: "resolution cannot be 0", 32 | } 33 | 34 | // errScaleXZero is returned when integer 0 is used as the X value 35 | // in a call to Scale. 36 | errScaleXZero = &OptionsError{ 37 | Option: "scale", 38 | Reason: "X scale cannot be 0", 39 | } 40 | 41 | // errScaleYZero is returned when integer 0 is used as the Y value 42 | // in a call to Scale. 43 | errScaleYZero = &OptionsError{ 44 | Option: "scale", 45 | Reason: "Y scale cannot be 0", 46 | } 47 | ) 48 | 49 | // OptionsError is an error which is returned when invalid input 50 | // options are set on a Waveform struct. 51 | type OptionsError struct { 52 | Option string 53 | Reason string 54 | } 55 | 56 | // Error returns the string representation of an OptionsError. 57 | func (e *OptionsError) Error() string { 58 | return fmt.Sprintf("%s: %s", e.Option, e.Reason) 59 | } 60 | 61 | // OptionsFunc is a function which is applied to an input Waveform 62 | // struct, and can manipulate its properties. 63 | type OptionsFunc func(*Waveform) error 64 | 65 | // SetOptions applies zero or more OptionsFunc to the receiving Waveform 66 | // struct, manipulating its properties. 67 | func (w *Waveform) SetOptions(options ...OptionsFunc) error { 68 | for _, o := range options { 69 | // Do not apply nil function arguments 70 | if o == nil { 71 | continue 72 | } 73 | 74 | if err := o(w); err != nil { 75 | return err 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // BGColorFunction generates an OptionsFunc which applies the input background 83 | // ColorFunc to an input Waveform struct. 84 | // 85 | // This function is used to apply a variety of color schemes to the background 86 | // of a waveform image, and is called during each drawing loop of the background 87 | // image. 88 | func BGColorFunction(function ColorFunc) OptionsFunc { 89 | return func(w *Waveform) error { 90 | return w.setBGColorFunction(function) 91 | } 92 | } 93 | 94 | // SetBGColorFunction applies the input ColorFunc to the receiving Waveform 95 | // struct for background use. 96 | func (w *Waveform) SetBGColorFunction(function ColorFunc) error { 97 | return w.SetOptions(BGColorFunction(function)) 98 | } 99 | 100 | // setBGColorFunction directly sets the background ColorFunc member of the 101 | // receiving Waveform struct. 102 | func (w *Waveform) setBGColorFunction(function ColorFunc) error { 103 | // Function cannot be nil 104 | if function == nil { 105 | return errBGColorFunctionNil 106 | } 107 | 108 | w.bgColorFn = function 109 | 110 | return nil 111 | } 112 | 113 | // FGColorFunction generates an OptionsFunc which applies the input foreground 114 | // ColorFunc to an input Waveform struct. 115 | // 116 | // This function is used to apply a variety of color schemes to the foreground 117 | // of a waveform image, and is called during each drawing loop of the foreground 118 | // image. 119 | func FGColorFunction(function ColorFunc) OptionsFunc { 120 | return func(w *Waveform) error { 121 | return w.setFGColorFunction(function) 122 | } 123 | } 124 | 125 | // SetFGColorFunction applies the input ColorFunc to the receiving Waveform 126 | // struct for foreground use. 127 | func (w *Waveform) SetFGColorFunction(function ColorFunc) error { 128 | return w.SetOptions(FGColorFunction(function)) 129 | } 130 | 131 | // setFGColorFunction directly sets the foreground ColorFunc member of the 132 | // receiving Waveform struct. 133 | func (w *Waveform) setFGColorFunction(function ColorFunc) error { 134 | // Function cannot be nil 135 | if function == nil { 136 | return errFGColorFunctionNil 137 | } 138 | 139 | w.fgColorFn = function 140 | 141 | return nil 142 | } 143 | 144 | // Resolution generates an OptionsFunc which applies the input resolution 145 | // value to an input Waveform struct. 146 | // 147 | // This value indicates the number of times audio is read and drawn 148 | // as a waveform, per second of audio. 149 | func Resolution(resolution uint) OptionsFunc { 150 | return func(w *Waveform) error { 151 | return w.setResolution(resolution) 152 | } 153 | } 154 | 155 | // SetResolution applies the input resolution to the receiving Waveform struct. 156 | func (w *Waveform) SetResolution(resolution uint) error { 157 | return w.SetOptions(Resolution(resolution)) 158 | } 159 | 160 | // setResolution directly sets the resolution member of the receiving Waveform 161 | // struct. 162 | func (w *Waveform) setResolution(resolution uint) error { 163 | // Resolution cannot be zero 164 | if resolution == 0 { 165 | return errResolutionZero 166 | } 167 | 168 | w.resolution = resolution 169 | 170 | return nil 171 | } 172 | 173 | // SampleFunc generates an OptionsFunc which applies the input SampleReduceFunc 174 | // to an input Waveform struct. 175 | // 176 | // This function is used to compute values from audio samples, for use in 177 | // waveform generation. The function is applied over a slice of float64 178 | // audio samples, reducing them to a single value. 179 | func SampleFunction(function SampleReduceFunc) OptionsFunc { 180 | return func(w *Waveform) error { 181 | return w.setSampleFunction(function) 182 | } 183 | } 184 | 185 | // SetSampleFunction applies the input SampleReduceFunc to the receiving Waveform 186 | // struct. 187 | func (w *Waveform) SetSampleFunction(function SampleReduceFunc) error { 188 | return w.SetOptions(SampleFunction(function)) 189 | } 190 | 191 | // setSampleFunction directly sets the SampleReduceFunc member of the receiving 192 | // Waveform struct. 193 | func (w *Waveform) setSampleFunction(function SampleReduceFunc) error { 194 | // Function cannot be nil 195 | if function == nil { 196 | return errSampleFunctionNil 197 | } 198 | 199 | w.sampleFn = function 200 | 201 | return nil 202 | } 203 | 204 | // Scale generates an OptionsFunc which applies the input X and Y axis scaling 205 | // factors to an input Waveform struct. 206 | // 207 | // This value indicates how a generated waveform image will be scaled, for both 208 | // its X and Y axes. 209 | func Scale(x uint, y uint) OptionsFunc { 210 | return func(w *Waveform) error { 211 | return w.setScale(x, y) 212 | } 213 | } 214 | 215 | // SetScale applies the input X and Y axis scaling to the receiving Waveform 216 | // struct. 217 | func (w *Waveform) SetScale(x uint, y uint) error { 218 | return w.SetOptions(Scale(x, y)) 219 | } 220 | 221 | // setScale directly sets the scaleX and scaleY members of the receiving Waveform 222 | // struct. 223 | func (w *Waveform) setScale(x uint, y uint) error { 224 | // X scale cannot be zero 225 | if x == 0 { 226 | return errScaleXZero 227 | } 228 | 229 | // Y scale cannot be zero 230 | if y == 0 { 231 | return errScaleYZero 232 | 233 | } 234 | 235 | w.scaleX = x 236 | w.scaleY = y 237 | 238 | return nil 239 | } 240 | 241 | // ScaleClipping generates an OptionsFunc which sets the scaleClipping member 242 | // to true on an input Waveform struct. 243 | // 244 | // This value indicates if the waveform image should be scaled down on its Y-axis 245 | // when clipping thresholds are reached. This can be used to show a more accurate 246 | // waveform when the input audio stream exhibits signs of clipping. 247 | func ScaleClipping() OptionsFunc { 248 | return func(w *Waveform) error { 249 | return w.setScaleClipping(true) 250 | } 251 | } 252 | 253 | // SetScaleClipping applies sets the scaleClipping member true for the receiving 254 | // Waveform struct. 255 | func (w *Waveform) SetScaleClipping() error { 256 | return w.SetOptions(ScaleClipping()) 257 | } 258 | 259 | // setScaleClipping directly sets the scaleClipping member of the receiving Waveform 260 | // struct. 261 | func (w *Waveform) setScaleClipping(scaleClipping bool) error { 262 | w.scaleClipping = scaleClipping 263 | 264 | return nil 265 | } 266 | 267 | // Sharpness generates an OptionsFunc which applies the input sharpness 268 | // value to an input Waveform struct. 269 | // 270 | // This value indicates the amount of curvature which is applied to a 271 | // waveform image, scaled on its X-axis. A higher value results in steeper 272 | // curves, and a lower value results in more "blocky" curves. 273 | func Sharpness(sharpness uint) OptionsFunc { 274 | return func(w *Waveform) error { 275 | return w.setSharpness(sharpness) 276 | } 277 | } 278 | 279 | // SetSharpness applies the input sharpness to the receiving Waveform struct. 280 | func (w *Waveform) SetSharpness(sharpness uint) error { 281 | return w.SetOptions(Sharpness(sharpness)) 282 | } 283 | 284 | // setSharpness directly sets the sharpness member of the receiving Waveform 285 | // struct. 286 | func (w *Waveform) setSharpness(sharpness uint) error { 287 | w.sharpness = sharpness 288 | 289 | return nil 290 | } 291 | -------------------------------------------------------------------------------- /waveform.go: -------------------------------------------------------------------------------- 1 | // Package waveform is capable of generating waveform images from audio streams. MIT Licensed. 2 | package waveform 3 | 4 | import ( 5 | "image" 6 | "image/color" 7 | "io" 8 | "math" 9 | 10 | "azul3d.org/engine/audio" 11 | 12 | // Import WAV and FLAC decoders 13 | _ "azul3d.org/engine/audio/flac" 14 | _ "azul3d.org/engine/audio/wav" 15 | ) 16 | 17 | const ( 18 | // imgYDefault is the default height of the generated waveform image 19 | imgYDefault = 128 20 | 21 | // scaleDefault is the default scaling factor used when scaling computed 22 | // value and waveform height by the output image's height 23 | scaleDefault = 3.00 24 | ) 25 | 26 | // Error values from azul3d/engine/audio are wrapped, so that callers do not 27 | // have to import an additional package to check for common errors. 28 | var ( 29 | // ErrFormat is returned when the input audio format is not a registered format 30 | // with the audio package. 31 | ErrFormat = audio.ErrFormat 32 | 33 | // ErrInvalidData is returned when the input audio format is recognized, but 34 | // the stream is invalid or corrupt in some way. 35 | ErrInvalidData = audio.ErrInvalidData 36 | 37 | // ErrUnexpectedEOS is returned when end-of-stream is encountered in the middle 38 | // of a fixed-size block or data structure. 39 | ErrUnexpectedEOS = audio.ErrUnexpectedEOS 40 | ) 41 | 42 | // Waveform is a struct which can be manipulated and used to generate 43 | // audio waveform images from an input audio stream. 44 | type Waveform struct { 45 | r io.Reader 46 | 47 | resolution uint 48 | sampleFn SampleReduceFunc 49 | 50 | bgColorFn ColorFunc 51 | fgColorFn ColorFunc 52 | 53 | scaleX uint 54 | scaleY uint 55 | 56 | sharpness uint 57 | 58 | scaleClipping bool 59 | } 60 | 61 | // Generate immediately opens and reads an input audio stream, computes 62 | // the values required for waveform generation, and returns a waveform image 63 | // which is customized by zero or more, variadic, OptionsFunc parameters. 64 | // 65 | // Generate is equivalent to calling New, followed by the Compute and Draw 66 | // methods of a Waveform struct. In general, Generate should only be used 67 | // for one-time waveform image generation. 68 | func Generate(r io.Reader, options ...OptionsFunc) (image.Image, error) { 69 | w, err := New(r, options...) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | values, err := w.Compute() 75 | return w.Draw(values), err 76 | } 77 | 78 | // New generates a new Waveform struct, applying any input OptionsFunc 79 | // on return. 80 | func New(r io.Reader, options ...OptionsFunc) (*Waveform, error) { 81 | // Generate Waveform struct with sane defaults 82 | w := &Waveform{ 83 | // Read from input stream 84 | r: r, 85 | 86 | // Read audio and compute values once per second of audio 87 | resolution: 1, 88 | 89 | // Use RMSF64Samples as a SampleReduceFunc 90 | sampleFn: RMSF64Samples, 91 | 92 | // Generate solid, black background color with solid, white 93 | // foreground color waveform using ColorFunc 94 | bgColorFn: SolidColor(color.White), 95 | fgColorFn: SolidColor(color.Black), 96 | 97 | // No scaling 98 | scaleX: 1, 99 | scaleY: 1, 100 | 101 | // Normal sharpness 102 | sharpness: 1, 103 | 104 | // Do not scale clipping values 105 | scaleClipping: false, 106 | } 107 | 108 | // Apply any input OptionsFunc on return 109 | return w, w.SetOptions(options...) 110 | } 111 | 112 | // Compute creates a slice of float64 values, computed using an input function. 113 | // 114 | // Compute is typically used once on an audio stream, to read and calculate the values 115 | // used for subsequent waveform generations. Its return value can be used with Draw to 116 | // generate and customize multiple waveform images from a single stream. 117 | func (w *Waveform) Compute() ([]float64, error) { 118 | return w.readAndComputeSamples() 119 | } 120 | 121 | // Draw creates a new image.Image from a slice of float64 values. 122 | // 123 | // Draw is typically used after a waveform has been computed one time, and a slice 124 | // of computed values was returned from the first computation. Subsequent calls to 125 | // Draw may be used to customize a waveform using the same input values. 126 | func (w *Waveform) Draw(values []float64) image.Image { 127 | return w.generateImage(values) 128 | } 129 | 130 | // readAndComputeSamples opens the input audio stream, computes samples according 131 | // to an input function, and returns a slice of computed values and any errors 132 | // which occurred during the computation. 133 | func (w *Waveform) readAndComputeSamples() ([]float64, error) { 134 | // Validate struct members 135 | // These checks are also done when applying options, but verifying them here 136 | // will prevent a runtime panic if called on an empty Waveform instance. 137 | if w.sampleFn == nil { 138 | return nil, errSampleFunctionNil 139 | } 140 | if w.resolution == 0 { 141 | return nil, errResolutionZero 142 | } 143 | 144 | // Open audio decoder on input stream 145 | decoder, _, err := audio.NewDecoder(w.r) 146 | if err != nil { 147 | // Unknown format 148 | if err == audio.ErrFormat { 149 | return nil, ErrFormat 150 | } 151 | 152 | // Invalid data 153 | if err == audio.ErrInvalidData { 154 | return nil, ErrInvalidData 155 | } 156 | 157 | // Unexpected end-of-stream 158 | if err == audio.ErrUnexpectedEOS { 159 | return nil, ErrUnexpectedEOS 160 | } 161 | 162 | // All other errors 163 | return nil, err 164 | } 165 | 166 | // computed is a slice of computed values by a SampleReduceFunc, from each 167 | // slice of audio samples 168 | var computed []float64 169 | 170 | // Track the current computed value 171 | var value float64 172 | 173 | // samples is a slice of float64 audio samples, used to store decoded values 174 | config := decoder.Config() 175 | samples := make(audio.Float64, uint(config.SampleRate*config.Channels)/w.resolution) 176 | for { 177 | // Decode at specified resolution from options 178 | // On any error other than end-of-stream, return 179 | _, err := decoder.Read(samples) 180 | if err != nil && err != audio.EOS { 181 | return nil, err 182 | } 183 | 184 | // Apply SampleReduceFunc over float64 audio samples 185 | value = w.sampleFn(samples) 186 | 187 | // Store computed value 188 | computed = append(computed, value) 189 | 190 | // On end of stream, stop reading values 191 | if err == audio.EOS { 192 | break 193 | } 194 | } 195 | 196 | // Return slice of computed values 197 | return computed, nil 198 | } 199 | 200 | // generateImage takes a slice of computed values and generates 201 | // a waveform image from the input. 202 | func (w *Waveform) generateImage(computed []float64) image.Image { 203 | // Store integer scale values 204 | intScaleX := int(w.scaleX) 205 | intScaleY := int(w.scaleY) 206 | 207 | // Calculate maximum n, x, y, where: 208 | // - n: number of computed values 209 | // - x: number of pixels on X-axis 210 | // - y: number of pixels on Y-axis 211 | maxN := len(computed) 212 | maxX := maxN * intScaleX 213 | maxY := imgYDefault * intScaleY 214 | 215 | // Create output, rectangular image 216 | img := image.NewRGBA(image.Rect(0, 0, maxX, maxY)) 217 | bounds := img.Bounds() 218 | 219 | // Calculate halfway point of Y-axis for image 220 | imgHalfY := bounds.Max.Y / 2 221 | 222 | // Calculate a peak value used for smoothing scaled X-axis images 223 | peak := int(math.Ceil(float64(w.scaleX)) / 2) 224 | 225 | // Calculate scaling factor, based upon maximum value computed by a SampleReduceFunc. 226 | // If option ScaleClipping is true, when maximum value is above certain thresholds 227 | // the scaling factor is reduced to show an accurate waveform with less clipping. 228 | imgScale := scaleDefault 229 | if w.scaleClipping { 230 | // Find maximum value from input slice 231 | var maxValue float64 232 | for _, c := range computed { 233 | if c > maxValue { 234 | maxValue = c 235 | } 236 | } 237 | 238 | // For each 0.05 maximum increment at 0.30 and above, reduce the scaling 239 | // factor by 0.25. This is a rough estimate and may be tweaked in the future. 240 | for i := 0.30; i < maxValue; i += 0.05 { 241 | imgScale -= 0.25 242 | } 243 | } 244 | 245 | // Values to be used for repeated computations 246 | var scaleComputed, halfScaleComputed, adjust int 247 | intBoundY := int(bounds.Max.Y) 248 | f64BoundY := float64(bounds.Max.Y) 249 | intSharpness := int(w.sharpness) 250 | 251 | // Begin iterating all computed values 252 | x := 0 253 | for n := range computed { 254 | // Scale computed value to an integer, using the height of the image and a constant 255 | // scaling factor 256 | scaleComputed = int(math.Floor(computed[n] * f64BoundY * imgScale)) 257 | 258 | // Calculate the halfway point for the scaled computed value 259 | halfScaleComputed = scaleComputed / 2 260 | 261 | // Draw background color down the entire Y-axis 262 | for y := 0; y < intBoundY; y++ { 263 | // If X-axis is being scaled, draw background over several X coordinates 264 | for i := 0; i < intScaleX; i++ { 265 | img.Set(x+i, y, w.bgColorFn(n, x+i, y, maxN, maxX, maxY)) 266 | } 267 | } 268 | 269 | // Iterate image coordinates on the Y-axis, generating a symmetrical waveform 270 | // image above and below the center of the image 271 | for y := imgHalfY - halfScaleComputed; y < scaleComputed+(imgHalfY-halfScaleComputed); y++ { 272 | // If X-axis is being scaled, draw computed value over several X coordinates 273 | for i := 0; i < intScaleX; i++ { 274 | // When scaled, adjust computed value to be lower on either side of the peak, 275 | // so that the image appears more smooth and less "blocky" 276 | if i < peak { 277 | // Adjust downward 278 | adjust = (i - peak) * intSharpness 279 | } else if i == peak { 280 | // No adjustment at peak 281 | adjust = 0 282 | } else { 283 | // Adjust downward 284 | adjust = (peak - i) * intSharpness 285 | } 286 | 287 | // On top half of the image, invert adjustment to create symmetry between 288 | // top and bottom halves 289 | if y < imgHalfY { 290 | adjust = -1 * adjust 291 | } 292 | 293 | // Retrieve and apply color function at specified computed value 294 | // count, and X and Y coordinates. 295 | // The output color is selected using the function, and is applied to 296 | // the resulting image. 297 | img.Set(x+i, y+adjust, w.fgColorFn(n, x+i, y+adjust, maxN, maxX, maxY)) 298 | } 299 | } 300 | 301 | // Increase X by scaling factor, to continue drawing at next loop 302 | x += intScaleX 303 | } 304 | 305 | // Return generated image 306 | return img 307 | } 308 | --------------------------------------------------------------------------------