Browse Source

Checkpoint

master
maze 5 years ago
parent
commit
1fbb267320
25 changed files with 1796 additions and 0 deletions
  1. +2
    -0
      .gitignore
  2. +2
    -0
      doc.go
  3. +82
    -0
      fft/fft.go
  4. +42
    -0
      file/midi/doc.go
  5. +61
    -0
      file/midi/drum.go
  6. +20
    -0
      file/midi/internal/midinote/main.go
  7. +100
    -0
      file/midi/midi.go
  8. +104
    -0
      file/midi/midi_test.go
  9. +135
    -0
      file/midi/note.go
  10. BIN
      file/wave/testdata/float.wav
  11. BIN
      file/wave/testdata/small.wav
  12. +220
    -0
      file/wave/wave.go
  13. +132
    -0
      file/wave/wave_test.go
  14. +196
    -0
      math/complex.go
  15. +2
    -0
      math/doc.go
  16. +65
    -0
      math/float.go
  17. +22
    -0
      math/int.go
  18. +9
    -0
      math/math.go
  19. +78
    -0
      math/rand.go
  20. +55
    -0
      pcm/gain.go
  21. +91
    -0
      pcm/interleave.go
  22. +27
    -0
      synth/doc.go
  23. +249
    -0
      synth/line.go
  24. +65
    -0
      synth/stream.go
  25. +37
    -0
      synth/type.go

+ 2
- 0
.gitignore View File

@ -24,3 +24,5 @@ _testmain.go
*.test
*.prof
# Testdata
testdata/code

+ 2
- 0
doc.go View File

@ -0,0 +1,2 @@
// Package dsp implements Digital Signal Processing
package dsp

+ 82
- 0
fft/fft.go View File

@ -0,0 +1,82 @@
// Package fft implements Fast-Fourier Transforms
package fft
import (
"errors"
"maze.io/dsp.v0/math"
)
// FFT performs a complex Fast-Fourier Transform
func FFT(x []complex128) ([]complex128, error) {
n := len(x)
if n <= 1 {
return x, nil
}
if n%2 != 0 {
return nil, errors.New("fft: length of x is not a power of 2")
}
h := n >> 1
// Even part
e := make([]complex128, n>>1)
for i := 0; i < h; i++ {
e[i] = x[i<<1]
}
if _, err := FFT(e); err != nil {
return nil, err
}
// Odd part
o := e
for i := 0; i < h; i++ {
o[i] = x[1+i<<1]
}
if _, err := FFT(o); err != nil {
return nil, err
}
f := make([]complex128, n)
for i := 0; i < h; i++ {
}
}
//
func Convolve(x, y []complex128) ([]complex128, error) {
if len(x) != len(y) {
return nil, errors.New("fft: x and y must have equal length")
}
n := len(x)
a := make([]complex128, n<<1)
b := make([]complex128, n<<1)
copy(a, x)
copy(b, y)
return ConvolveCirculair(a, b)
}
//
func ConvolveCirculair(x, y []complex128) ([]complex128, error) {
if len(x) != len(y) {
return nil, errors.New("fft: x and y must have equal length")
}
n := len(x)
if a, err := FFT(x); err != nil {
return nil, err
}
if b, err := FFT(y); err != nil {
return nil, err
}
c := make([]complex128, n)
for i := 0; i < n; i++ {
c[i] = math.ComplexMul(a[i], b[i])
}
return IFFT(c)
}

+ 42
- 0
file/midi/doc.go View File

@ -0,0 +1,42 @@
/*
Package midi implements the Musical Instrument Digital Interface.
This package offers note to frequency transformations, frequency to note
transformations and note parsing routines.
A note on octaves
The MIDI specification only defines note number 60 as "Middle C", and all
other notes are relative. The absolute octave number designations shown here
are based on Middle C = C5, which is an arbitrary assignment.
There is a discrepancy that occurs between various models of MIDI devices
and software programs, and that concerns the octave numbers for note names.
If your MIDI software/device considers octave 0 as being the lowest octave
of the MIDI note range, then middle C's note name is C5. The lowest note name
is then C0 (note number 0), and the highest possible note name is G10
(note number 127).
Some software/devices instead consider the third octave of the MIDI note
range (2 octaves below middle C) as octave 0. In that case, the first 2
octaves are referred to as -2 and -1. So, middle C's note name is C3, the
lowest note name is C-2, and the highest note name is G8.
The numbers used are 0 to 127. The lowest note upon a MIDI controller is a C
and this is assigned note number 0. The C# above it would have a note number
of 1. The D note above that would have a note number of 2. So "Middle C" is
note number 60. A MIDI note number of 69 is used for A440 tuning, that is
the A note above middle C.
References
The complete MIDI 1.0 detailed specification, © Copyright The MIDI Association
https://www.midi.org/specifications/item/the-midi-1-0-specification
Standard MIDI-File Format Spec. 1.1, updated, © Copyright 1999 David Back
http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html
MIDI Note Numbers for Different Octaves, © Copyright 2005 Thomas Scarff
http://www.electronics.dit.ie/staff/tscarff/Music_technology/midi/midi_note_numbers_for_octaves.htm
*/
package midi

+ 61
- 0
file/midi/drum.go View File

@ -0,0 +1,61 @@
package midi
// Drums
var (
Drum = map[string]Note{}
DrumName = map[Note]string{
B1: "acoustic bass drum",
C2: "bass drum 1",
Cs2: "side stick",
D2: "acoustic snare",
Ds2: "hand clap",
E2: "electric snare",
F2: "low floor tom",
Fs2: "closed hi hat",
G2: "high floor tom",
Gs2: "pedal hi-hat",
A2: "low tom",
As2: "open hi-hat",
B2: "low-mid tom",
C3: "hi mid tom",
Cs3: "crash cymbal 1",
D3: "high tom",
Ds3: "ride cymbal 1",
E3: "chinese cymbal",
F3: "ride bell",
Fs3: "tambourine",
G3: "splash cymbal",
Gs3: "cowbell",
A3: "crash cymbal 2",
As3: "vibraslap",
B3: "ride cymbal",
C4: "hi bongo",
Cs4: "low bongo",
D4: "mute hi conga",
Ds4: "open hi conga",
E4: "low conga",
F4: "high timbale",
Fs4: "low timbale",
G4: "high agogo",
Gs4: "low agogo",
A4: "cabasa",
As4: "maracas",
B4: "short whistle",
C5: "long whistle",
Cs5: "short guiro",
D5: "long guiro",
Ds5: "claves",
E5: "hi wood block",
F5: "low wood block",
Fs5: "mute cuica",
G5: "open cuica",
Gs5: "mute triangle",
A5: "open triangle",
}
)
func init() {
for n, s := range DrumName {
Drum[s] = n
}
}

+ 20
- 0
file/midi/internal/midinote/main.go View File

@ -0,0 +1,20 @@
package main
import "fmt"
var noteNames = [12]string{"C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B"}
func main() {
fmt.Println("//go:generate go run internal/midinote/main.go > note.go")
fmt.Println("")
fmt.Println("package midi")
fmt.Println("")
fmt.Println("// Default MIDI notes")
fmt.Println("const (")
fmt.Println("\tC0 Note = iota // 0")
for i := 1; i < 128; i++ {
n := fmt.Sprintf("%s%d", noteNames[i%12], i/12)
fmt.Printf("\t%-15s // %d\n", n, i)
}
fmt.Println(")")
}

+ 100
- 0
file/midi/midi.go View File

@ -0,0 +1,100 @@
package midi
import (
"errors"
"fmt"
"log"
"math"
"strconv"
"strings"
"maze.io/dsp.v0/synth"
)
const (
// A440 is the frequency of the A4 MIDI note
A440 synth.Frequency = 440.0
)
var (
ErrNoteInvalid = errors.New("dsp: invalid MIDI note")
)
// Note describes a pitch
type Note int8
var (
nameDelta = map[byte]int8{'c': 0, 'd': 2, 'e': 4, 'f': 5, 'g': 7, 'a': 9, 'b': 11}
accidentDelta = map[byte]int8{'b': -1, '#': 1, 'x': 2}
)
func ParseNote(s string) (Note, error) {
s = strings.TrimSpace(strings.ToLower(s))
if len(s) < 2 {
return 0, ErrNoteInvalid
}
var (
l = int8(len(s))
i int8 // offset
n int8 // note delta
a int8 // accidents sum
ad int8 // accidents delta
o int // octave delta
ok bool
err error
)
// Parse note name
if n, ok = nameDelta[s[0]]; !ok {
log.Println("error name")
return 0, ErrNoteInvalid
}
i++
// Parse accidents
for ; i < l; i++ {
if ad, ok = accidentDelta[s[i]]; ok {
a += ad
} else {
break
}
}
// Parse octave
if o, err = strconv.Atoi(s[i:]); err != nil {
return 0, ErrNoteInvalid
}
return Note(n) + Note(a) + Note(12*o), nil
}
func ParseNoteFrequency(f synth.Frequency) (Note, error) {
n := 69 + 12*math.Log2(float64(f)/float64(A440))
if n >= 127.5 {
return 0, ErrNoteInvalid
}
return Note(math.Floor(n + .5)), nil
}
// Frequency of the note
func (n Note) Frequency() synth.Frequency {
//f := math.Pow(float64(A440)*2, (float64(n)-float64(A4))*(1/12))
f := math.Pow(2, float64(n-69)/12) * float64(A440)
return synth.Frequency(f)
}
// Octave of the note in range [-1, 9)
func (n Note) Octave() int8 {
return int8(n) / 12
}
var noteNames = [12]string{"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}
func (n Note) String() string {
return fmt.Sprintf("%s%d", noteNames[n%12], n.Octave())
}
// Velocity describes the gain (volume) of a note
type Velocity byte

+ 104
- 0
file/midi/midi_test.go View File

@ -0,0 +1,104 @@
package midi
import (
"fmt"
"strings"
"testing"
"maze.io/dsp.v0/math"
"maze.io/dsp.v0/synth"
)
func ExampleParseNote() {
for _, s := range strings.Split("A4,G4,F4,G4,A4,A4,A4", ",") {
n, err := ParseNote(s)
if err != nil {
panic(err)
}
fmt.Printf("note %q parsed to %q", s, n)
}
}
func TestNoteFrequency(t *testing.T) {
testCases := []struct {
n Note
f synth.Frequency
}{
{A5, A440},
{A1, 27.5},
{C5, 261.625565},
}
t.Run("NoteToFrequency", func(t *testing.T) {
for _, testCase := range testCases {
f := testCase.n.Frequency()
if !math.FloatCompare(float64(f), float64(testCase.f), 1e3) {
t.Fatalf("(%s).Frequency returned %f, expected %f", testCase.n, f, testCase.f)
}
t.Logf("(%s).Frequency = %s", testCase.n, f)
}
})
t.Run("FrequencyToNote", func(t *testing.T) {
for _, testCase := range testCases {
n, err := ParseNoteFrequency(testCase.f)
if err != nil {
t.Fatalf("ParseNoteFrequency(%f) failed: %v", testCase.f, err)
}
t.Logf("ParseNoteFrequency(%f) = %s", testCase.f, n)
}
testCasesSlightlyOff := []struct {
n Note
f synth.Frequency
}{
{A5, A440 + 0.499999},
{A1, 27.666},
{C5, 261.7},
}
for _, testCase := range testCasesSlightlyOff {
n, err := ParseNoteFrequency(testCase.f)
if err != nil {
t.Fatalf("ParseNoteFrequency(%f) failed: %v", testCase.f, err)
}
t.Logf("ParseNoteFrequency(%f) = %s", testCase.f, n)
}
})
}
func TestParseNote(t *testing.T) {
t.Run("valid", func(t *testing.T) {
testCases := []struct {
s string
n Note
}{
{"C0", C0},
{"C#0", Cs0},
{"A4", A4},
{"A#4", As4},
{"Fb3", E3},
{"Fb#3", F3},
{"Fx5", G5},
}
for _, testCase := range testCases {
n, err := ParseNote(testCase.s)
if err != nil {
t.Fatalf("ParseNote(%q): %v", testCase.s, err)
}
if n != testCase.n {
t.Fatalf("ParseNote(%q) returned %s, expected %s", testCase.s, n, testCase.n)
}
t.Logf("ParseNote(%q): %s", testCase.s, n)
}
})
t.Run("invalid", func(t *testing.T) {
testCases := []string{"", "0", "C", "CC0"}
for _, testCase := range testCases {
n, err := ParseNote(testCase)
if err != nil {
t.Logf("ParseNote(%q): %v (expected)", testCase, err)
continue
}
t.Fatalf("ParseNote(%q): returned %s, expected error", testCase, n)
}
})
}

+ 135
- 0
file/midi/note.go View File

@ -0,0 +1,135 @@
//go:generate go run internal/midinote/main.go > note.go
package midi
// Default MIDI notes
const (
C0 Note = iota
Cs0
D0
Ds0
E0
F0
Fs0
G0
Gs0
A0
As0
B0
C1
Cs1
D1
Ds1
E1
F1
Fs1
G1
Gs1
A1
As1
B1
C2
Cs2
D2
Ds2
E2
F2
Fs2
G2
Gs2
A2
As2
B2
C3
Cs3
D3
Ds3
E3
F3
Fs3
G3
Gs3
A3
As3
B3
C4
Cs4
D4
Ds4
E4
F4
Fs4
G4
Gs4
A4
As4
B4
C5
Cs5
D5
Ds5
E5
F5
Fs5
G5
Gs5
A5
As5
B5
C6
Cs6
D6
Ds6
E6
F6
Fs6
G6
Gs6
A6
As6
B6
C7
Cs7
D7
Ds7
E7
F7
Fs7
G7
Gs7
A7
As7
B7
C8
Cs8
D8
Ds8
E8
F8
Fs8
G8
Gs8
A8
As8
B8
C9
Cs9
D9
Ds9
E9
F9
Fs9
G9
Gs9
A9
As9
B9
C10
Cs10
D10
Ds10
E10
F10
Fs10
G10
)

BIN
file/wave/testdata/float.wav View File


BIN
file/wave/testdata/small.wav View File


+ 220
- 0
file/wave/wave.go View File

@ -0,0 +1,220 @@
package wave
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"time"
)
var ErrUnknownAudioFormat = errors.New("dsp: unknown WAVE audio format")
// Supported formats.
const (
FormatInvalid = iota
FormatPCM
_
FormatIEEEFloat
)
// Header of a WAVE file.
type Header struct {
AudioFormat uint16
Channels uint16
SampleRate uint32
ByteRate uint32
BlockAlign uint16
BitsPerSample uint16
}
type Decoder struct {
Header
samples int
duration time.Duration
r io.Reader
}
func NewDecoder(r io.Reader) (*Decoder, error) {
b := make([]byte, 16)
if _, err := io.ReadFull(r, b[:12]); err != nil {
return nil, err
}
if string(b[0:4]) != "RIFF" {
return nil, fmt.Errorf("dsp: not a RIFF file")
}
if string(b[8:12]) != "WAVE" {
return nil, fmt.Errorf("dsp: not a RIFF WAVE file")
}
d := new(Decoder)
hasFmt := false
for {
if _, err := io.ReadFull(r, b[:8]); err != nil {
return nil, err
}
sz := binary.LittleEndian.Uint32(b[4:])
switch typ := string(b[:4]); typ {
case "fmt ":
if sz < 16 {
return nil, fmt.Errorf("wav: bad fmt size")
}
f := make([]byte, sz)
if _, err := io.ReadFull(r, f); err != nil {
return nil, err
}
if err := binary.Read(bytes.NewBuffer(f), binary.LittleEndian, &d.Header); err != nil {
return nil, err
}
switch d.AudioFormat {
case FormatPCM:
case FormatIEEEFloat:
default:
return nil, fmt.Errorf("dsp: unknown audio format: %02x", d.AudioFormat)
}
hasFmt = true
case "data":
if !hasFmt {
return nil, fmt.Errorf("dsp: unexpected fmt chunk")
}
d.samples = int(sz) / int(d.BitsPerSample) * 8
d.duration = time.Duration(d.samples) * time.Second / time.Duration(d.SampleRate) / time.Duration(d.Channels)
d.r = io.LimitReader(r, int64(sz))
return d, nil
default:
io.CopyN(ioutil.Discard, r, int64(sz))
}
}
}
func (d *Decoder) read(p []byte) (int, error) {
return io.ReadFull(d.r, p)
}
// Read
func (d *Decoder) Read(p []byte) (int, error) {
switch d.AudioFormat {
case FormatPCM:
switch d.BitsPerSample {
case 8:
return d.read(p)
case 16:
b, err := d.readInts(len(p))
if err != nil {
return len(b), err
}
for i, n := range b {
p[i] = byte(n)
}
return len(b), nil
default:
return 0, fmt.Errorf("dsp: invalid bits per sample %d", d.BitsPerSample)
}
case FormatIEEEFloat:
f, err := d.readFloats(len(p))
if err != nil {
return len(f) * 4, err
}
for i, n := range f {
// range [-1.0, 1.0) to [0, 256)
p[i] = byte((n + 1.0) * 128.0)
}
return len(p) * 4, nil
}
return 0, ErrUnknownAudioFormat
}
func (d *Decoder) readInts(n int) ([]int16, error) {
u := make([]int16, n)
if err := binary.Read(d.r, binary.LittleEndian, u); err != nil {
return nil, err
}
return u, nil
}
func (d *Decoder) ReadInts(n int) ([]int16, error) {
switch d.AudioFormat {
case FormatPCM:
switch d.BitsPerSample {
case 8:
p := make([]byte, n)
if _, err := d.read(p); err != nil {
return nil, err
}
o := make([]int16, n)
for i, n := range p {
if n >= 0x7f {
o[i] = int16(n) << 2
} else {
o[i] = -int16(n) << 2
}
}
return o, nil
case 16:
return d.readInts(n)
default:
return nil, fmt.Errorf("dsp: invalid bits per sample %d", d.BitsPerSample)
}
case FormatIEEEFloat:
f, err := d.readFloats(n)
if err != nil {
return nil, err
}
o := make([]int16, n)
for i, n := range f {
// range [-1, 1) to [-256, 256)
o[i] = int16(n * 256)
}
return o, nil
}
return nil, ErrUnknownAudioFormat
}
func (d *Decoder) readFloats(n int) ([]float32, error) {
f := make([]float32, n)
if err := binary.Read(d.r, binary.LittleEndian, f); err != nil {
return nil, err
}
return f, nil
}
const int16c = float32(math.MaxInt16 - math.MinInt16)
func (d *Decoder) ReadFloats(n int) ([]float32, error) {
switch d.AudioFormat {
case FormatPCM:
switch d.BitsPerSample {
case 8:
p := make([]byte, n)
if _, err := d.read(p); err != nil {
return nil, err
}
f := make([]float32, n)
for i, n := range p {
f[i] = float32(n) / math.MaxUint8
}
return f, nil
case 16:
u, err := d.readInts(n)
if err != nil {
return nil, err
}
f := make([]float32, n)
for i, n := range u {
// range [-256, 256) to [-1.0, 1.0)
f[i] = float32(n) / 256.0
}
return f, nil
default:
return nil, fmt.Errorf("dsp: invalid bits per sample %d", d.BitsPerSample)
}
case FormatIEEEFloat:
return d.readFloats(n)
}
return nil, ErrUnknownAudioFormat
}

+ 132
- 0
file/wave/wave_test.go View File

@ -0,0 +1,132 @@
package wave
import (
"bytes"
"encoding/hex"
"os"
"path"
"reflect"
"testing"
)
type testWave struct {
d Decoder
skip int
b []byte
u []int16
f []float32
}
func TestWAVE(t *testing.T) {
testCases := map[string]testWave{
"testdata/float.wav": {
d: Decoder{
Header: Header{
AudioFormat: 3,
Channels: 1,
SampleRate: 44100,
ByteRate: 176400,
BlockAlign: 4,
BitsPerSample: 32,
},
samples: 1889280 / 4,
duration: 10710204081,
},
skip: 10240,
b: []byte{0x76, 0x75, 0x75, 0x75, 0x74, 0x74, 0x74, 0x74, 0x73, 0x73, 0x73, 0x73, 0x73, 0x73, 0x72, 0x72, 0x72, 0x72, 0x72, 0x72, 0x72, 0x72, 0x72, 0x73, 0x73, 0x73, 0x73, 0x73, 0x73, 0x74, 0x74, 0x74, 0x74, 0x74, 0x75, 0x75, 0x75, 0x75, 0x75, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x78, 0x78, 0x78},
u: []int16{-14, -13, -13, -12, -12, -11, -10, -10, -9, -8, -8, -7, -6, -5, -5, -4, -4, -3, -2, -2, -1, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 7, 7, 8, 9, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 30, 31, 32},
f: []float32{0.12723188, 0.1291177, 0.13078508, 0.13224564, 0.13347009, 0.13439086, 0.13494965, 0.13513772, 0.13501705, 0.13468324, 0.13422318, 0.13365218, 0.13298117, 0.1322022, 0.13132581, 0.13036773, 0.12930594, 0.12810025, 0.12672019, 0.12518774, 0.123543166, 0.12182657, 0.120071284, 0.11826183, 0.116404995, 0.11451198, 0.11257407, 0.11056005, 0.10838329, 0.10596584, 0.103271514, 0.100338325, 0.09723275, 0.09401177, 0.09068273, 0.08722351, 0.083610624, 0.079836205, 0.07589425, 0.07175553, 0.06736887, 0.062708735, 0.057780262, 0.05262575, 0.0473044, 0.041841075, 0.036244515, 0.030536082, 0.02473928, 0.018874813, 0.012925628, 0.0068357363, 0.00054879487, -0.0059488267, -0.012586582, -0.019277275, -0.025927208, -0.032501195, -0.03899248, -0.04539113, -0.05167974, -0.057833783, -0.06386266, -0.0697867},
},
"testdata/small.wav": {
d: Decoder{
Header: Header{
AudioFormat: 1,
Channels: 1,
SampleRate: 44100,
ByteRate: 88200,
BlockAlign: 2,
BitsPerSample: 16,
},
samples: 41888,
duration: 949841269,
},
skip: 10240,
b: []byte{0xe3, 0xe3, 0xe3, 0xf6, 0xf6, 0xda, 0xda, 0xda, 0xe1, 0xe1, 0xe1, 0xe6, 0xe6, 0xe6, 0xe7, 0xe7, 0x9, 0x9, 0x9, 0x11, 0x11, 0x11, 0x1, 0x1, 0x1, 0xd8, 0xd8, 0xed, 0xed, 0xed, 0xf6, 0xf6},
u: []int16{-10, -22, -22, -22, 7, 7, 5, 5, 5, -18, -18, -18, -26, -26, -26, -9, -9, -7, -7, -7, -15, -15, -15, -26, -26, -26, -4, -4, 0, 0, 0, 14},
f: []float32{0.0546875, 0.0546875, -0.03125, -0.03125, -0.03125, 0.015625, 0.015625, -0.10546875, -0.10546875, -0.10546875, -0.1796875, -0.1796875, -0.1796875, -0.09765625, -0.09765625, -0.09765625, -0.0703125, -0.0703125, 0, 0, 0, 0.0703125, 0.0703125, 0.0703125, 0.1015625, 0.1015625, 0.1015625, -0.03125, -0.03125, -0.03125, 0.17578125, 0.17578125},
},
}
for name, testCase := range testCases {
t.Run(path.Base(name), func(t *testing.T) {
f, err := os.Open(name)
if err != nil {
t.Fatal(err)
}
defer f.Close()
d, err := NewDecoder(f)
if err != nil {
t.Fatal(err)
}
r := d.r
d.r = nil
if *d != testCase.d {
t.Fatalf("WAVE not equal, got %+v, want %+v", *d, testCase.d)
}
t.Logf("%+v", *d)
d.r = r
if testCase.skip > 0 {
if _, err := d.r.Read(make([]byte, testCase.skip)); err != nil {
t.Fatal(err)
}
}
if testCase.b != nil {
t.Run("read", func(t *testing.T) {
p := make([]byte, len(testCase.b))
if _, err := d.Read(p); err != nil {
t.Fatal(err)
}
if !bytes.Equal(testCase.b, p) {
t.Log("WAVE bytes differ")
t.Logf("expected:\n%s", hex.Dump(testCase.b))
t.Fatalf("got:\n%s", hex.Dump(p))
}
})
}
if testCase.u != nil {
t.Run("ints", func(t *testing.T) {
u, err := d.ReadInts(len(testCase.u))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(testCase.u, u) {
t.Log("WAVE ints differ")
t.Logf("expected: %+v", testCase.u)
t.Fatalf("got: %+v", u)
}
})
}
if testCase.f != nil {
t.Run("floats", func(t *testing.T) {
f, err := d.ReadFloats(len(testCase.u))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(testCase.f, f) {
t.Log("WAVE floats differ")
t.Logf("expected: %+v", testCase.f)
t.Fatalf("got: %+v", f)
}
})
}
})
}
}

+ 196
- 0
math/complex.go View File

@ -0,0 +1,196 @@
package math
import (
"errors"
"fmt"
)
func ComplexAdd(x, y complex128) complex128 {
return complex(real(x)+real(y), imag(x)+imag(y))
}
func ComplexMul(x, y complex128) complex128 {
r := real(x)*real(y) - imag(x)*imag(y)
i := real(x)*imag(y) + imag(x)*real(y)
return complex(r, i)
}
func ComplexSub(x, y complex128) complex128 {
return complex(real(x)-real(y), imag(x)-imag(y))
}
// ComplexCompare compares two complex slices within a tolerance.
func ComplexCompare(a, b complex128, epsilon float64) bool {
return FloatCompare(real(a), real(b), epsilon) && FloatCompare(imag(a), imag(b), epsilon)
}
// ComplexCompares compares two complex slices within a tolerance.
func ComplexCompares(a, b []complex128, epsilon float64) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if !ComplexCompare(v, b[i], epsilon) {
return false
}
}
return true
}
// ComplexCompare2D compares two complex 2D-matrices within a tolerance.
func ComplexCompare2D(a, b [][]complex128, epsilon float64) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if !ComplexCompares(v, b[i], epsilon) {
return false
}
}
return true
}
// ComplexMatrix is a multidimensional matrix.
type ComplexMatrix struct {
m []complex128
dims, offs []int
}
func NewComplexMatrix(dims []int) *ComplexMatrix {
x := 1
for _, v := range dims {
x *= v
}
m, _ := ComplexMatrixBuild(make([]complex128, x), dims)
return m
}
// ComplexMatrixBuild builds a multidimensional matrix from a slice with the
// specified dimensions.
func ComplexMatrixBuild(x []complex128, dims []int) (*ComplexMatrix, error) {
l := 1
offs := make([]int, len(dims))
for i := len(dims) - 1; i >= 0; i-- {
if dims[i] < 1 {
return nil, errors.New("dsp: matrix dimensions must be >= 1")
}
offs[i] = l
l *= dims[i]
}
if len(x) != l {
return nil, errors.New("dsp: incorrect dimensions")
}
m := &ComplexMatrix{
m: x,
dims: make([]int, len(dims)),
offs: offs,
}
copy(m.dims, dims)
return m, nil
}
// ComplexMatrixBuild2D builds a 2D-matrix from a slice.
func ComplexMatrixBuild2D(x [][]complex128) (*ComplexMatrix, error) {
dims := []int{len(x), len(x[0])}
m := make([]complex128, dims[0]*dims[1])
for r, v := range x {
if len(v) != dims[1] {
return nil, fmt.Errorf("dsp: row %d contains %d values, matrix is %dx%d", r, len(v), dims[0], dims[1])
}
copy(m[r*dims[1]:(r+1)*dims[1]], v)
}
return ComplexMatrixBuild(m, dims)
}
// Compare two complex multidimensional matrices within a tolerance.
func (m *ComplexMatrix) Compare(o *ComplexMatrix, epsilon float64) bool {
for r, v := range m.dims {
if v != o.dims[r] {
return false
}
}
return ComplexCompares(m.m, o.m, epsilon)
}
// Dims returns dimensions of the matrix.
func (m *ComplexMatrix) Dims() []int {
d := make([]int, len(m.dims))
copy(d, m.dims)
return d
}
// EqualTo checks the equality of two matrices.
func (m *ComplexMatrix) EqualTo(o *ComplexMatrix) bool {
// Fast path
if m == o {
return true
}
for r, v := range m.dims {
if v != o.dims[r] {
return false
}
}
for i, x := range m.m {
if x != o.m[i] {
return false
}
}
return true
}
func (m *ComplexMatrix) offset(dims []int) (i int, err error) {
if len(dims) != len(m.dims) {
err = fmt.Errorf("dsp: expected %d dims, got %d", len(m.dims), len(dims))
return
}
for n, v := range dims {
if v > m.dims[n] {
err = errors.New("dsp: invalid dimensions")
return
}
i += v * m.offs[n]
}
return
}
func (m *ComplexMatrix) SetValue(dims []int, x complex128) error {
o, err := m.offset(dims)
if err != nil {
return err
}
m.m[o] = x
return nil
}
func (m *ComplexMatrix) Value(dims []int) (complex128, error) {
o, err := m.offset(dims)
if err != nil {
return 0, err
}
return m.m[o], nil
}
// H returns the conjugate (Hermitian) matrix.
func (m *ComplexMatrix) H() (*ComplexMatrix, error) {
if len(m.dims) != 2 {
return nil, errors.New("dsp: not a 2D matrix")
}
panic("not implemented")
}

+ 2
- 0
math/doc.go View File

@ -0,0 +1,2 @@
// Package math implements common mathematical operations.
package math

+ 65
- 0
math/float.go View File

@ -0,0 +1,65 @@
package math
import "math"
// FloatCompare compares two float slices within a tolerance.
func FloatCompare(a, b float64, epsilon float64) bool {
return math.Abs(a-b) <= epsilon || math.Abs(1-a/b) <= epsilon
}
// FloatCompares compares two float slices within a tolerance.
func FloatCompares(a, b []float64, epsilon float64) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if !FloatCompare(v, b[i], epsilon) {
return false
}
}
return true
}
// FloatCompare2D compares two float 2D-matrices within a tolerance.
func FloatCompare2D(a, b [][]float64, epsilon float64) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if !FloatCompares(v, b[i], epsilon) {
return false
}
}
return true
}
// FloatsToComplex converts a float slice to a complex slice, where
// the float will be the complex real value.
func FloatsToComplex(f []float64) []complex128 {
x := make([]complex128, len(f))
for i, v := range f {
x[i] = complex(v, 0)
}
return x
}
// FloatsToComplexMatrix converts a float matrix to a complex matrix, where
// the float will be the complex real value.
func FloatsToComplexMatrix(f [][]float64) [][]complex128 {
x := make([][]complex128, len(f))
for i, v := range f {
x[i] = FloatsToComplex(v)
}
return x
}
// FloatsZero sets all float slice elements to zero.
func FloatsZero(f []float64) {
for i := range f {
f[i] = 0
}
}

+ 22
- 0
math/int.go View File

@ -0,0 +1,22 @@
package math
// IsPowerOf2 returns true if n is a power of 2.
func IsPowerOf2(n int) bool {
return n&(n-1) == 0
}
// Min returns the smallest value of a and b.
func Min(a, b int) int {
if a < b {
return a
}
return b
}
// Max returns the largest value of a and b.
func Max(a, b int) int {
if a > b {
return a
}
return b
}

+ 9
- 0
math/math.go View File

@ -0,0 +1,9 @@
package math
import "math"
// Aliases
const (
E = math.E
Pi = math.Pi
)

+ 78
- 0
math/rand.go View File

@ -0,0 +1,78 @@
package math
import (
"fmt"
"math"
"math/rand"
"sync"
)
type UniformRandom struct {
m *sync.Mutex
r *rand.Rand
}
func NewUniformRandom(seed int64) *UniformRandom {
return &UniformRandom{
m: new(sync.Mutex),
r: rand.New(rand.NewSource(seed)),
}
}
// Float32 returns a random float32 in [0.0, 1.0)
func (r UniformRandom) Float32() float32 {
r.m.Lock()
f := r.r.Float32()
r.m.Unlock()
return f
}
// Float64 returns a random float64 in [0.0, 1.0)
func (r UniformRandom) Float64() float64 {
r.m.Lock()
f := r.r.Float64()
r.m.Unlock()
return f
}
// Float32Range returns a random float32 in [a, b)
func (r UniformRandom) Float32Range(a, b float32) float32 {
if !(a < b) {
panic(fmt.Sprintf("dsp: invalid range: %.2f ~ %.2f", a, b))
}
return a + r.Float32()*(b-a)
}
// Float64Range returns a random float32 in [a, b)
func (r UniformRandom) Float64Range(a, b float64) float64 {
if !(a < b) {
panic(fmt.Sprintf("dsp: invalid range: %.2f ~ %.2f", a, b))
}
return a + r.Float64()*(b-a)
}
// Float32n returns a random float32 in [0.0, n)
func (r UniformRandom) Float32n(n float32) float32 {
return r.Float32Range(0.0, n)
}
// Float64n returns a random float64 in [0.0, n)
func (r UniformRandom) Float64n(n float64) float64 {
return r.Float64Range(0.0, n)
}
// Gaussian returns a random number of gaussian distribution Gauss(mean, stddev^2)
func (r UniformRandom) Gaussian(mu, sigma float64) float64 {
return mu + sigma*r.gaussian()
}
func (r UniformRandom) gaussian() float64 {
// Box-Muller Transform
var f, x, y float64
for f >= 1 || f == 0 {
x = r.Float64Range(-1.0, 1.0)
y = r.Float64Range(-1.0, 1.0)
f = x*x + y*y
}
return x * math.Sqrt(-2*math.Log(f)/f)
}

+ 55
- 0
pcm/gain.go View File

@ -0,0 +1,55 @@
package pcm
import "math"
// FloatAGC implements Automatic Gain Control, value settles in range (-0.5, +0.5).
type FloatAGC struct {
Attack float64
Decay float64
peak, valley float64
}
// NewFloatAGCC starts a new FloatAGC block.
func NewFloatAGC(attack, decay float64) *FloatAGC {
return &FloatAGC{
Attack: attack,
Decay: decay,
}
}
// Process a sample.
func (agc *FloatAGC) Process(in float64) float64 {
if in >= agc.peak {
agc.peak = in*agc.Attack + agc.peak*(1-agc.Attack)
} else {
agc.peak = in*agc.Decay + agc.peak*(1-agc.Decay)
}
if in <= agc.valley {
agc.valley = in*agc.Attack + agc.valley*(1-agc.Attack)
} else {
agc.valley = in*agc.Decay + agc.valley*(1-agc.Decay)
}
if agc.peak > agc.valley {
return in - 0.5*(agc.peak+agc.valley)/(agc.peak-agc.valley)
}
return 0
}
// DBSquaredAmplitude converts from squared amplitude to power gain.
func DBSquaredAmplitude(gain float64) (db float64) {
if gain == 0 {
return math.Inf(-1)
}
return 10 * math.Log10(float64(gain))
}
// DBAmplitude converts from amplitude to power gain.
func DBAmplitude(gain float64) (db float64) {
if gain == 0 {
return math.Inf(-1)
}
return 20 * math.Log10(float64(gain))
}

+ 91
- 0
pcm/interleave.go View File

@ -0,0 +1,91 @@
package pcm
import (
"errors"
"math"
)
// FloatInterleave interleaves left and right (dual mono) channels to a stereo
// interleaved channel.
func FloatInterleave(l, r []float64) ([]float64, error) {
if len(l) != len(r) {
return nil, errors.New("dsp: left and right channel must contain equal number of samples")
}
n := len(l)
o := make([]float64, n<<1)
for i := 0; i < n; i {
p := i << 1
o[p] = l[i]
o[p+1] = r[i]
}
return o, nil
}
type FloatSamples []float64
// Deinterleave a stereo interleaved channel to dual mono left and right.
func (s FloatSamples) Deinterleave() ([]float64, []float64, error) {
n := len(s)
if n%2 != 0 {
return nil, nil, errors.New("dsp: channel of uneven length")
}
l := make([]float64, n>>1)
r := make([]float64, n>>1)
var p int
for i := 0; i < n; i++ {
l[i] = s[p]
p++
r[i] = s[p]
p++
}
return l, r, nil
}
// RMS value of a buffer.
func (s FloatSamples) RMS() float64 {
var (
r, v float64
i int
)
for i, v = range s {
r += v * v
}
return math.Sqrt(r / float64(i))
}
// Peak value of a buffer.
func (s FloatSamples) Peak() float64 {
p := math.Inf(-1)
for _, v := range s {
if a := math.Abs(v); a > p {
p = a
}
}
return p
}
// FloatMix mixes two sample buffers normalized by a constant.
func FloatMix(a, b []float64, negate bool, normalize float64) ([]float64, error) {
if len(a) != len(b) {
return nil, errors.New("dsp: channels must contain equal number of samples")
}
o := make([]float64, len(a))
copy(o, a)
if negate {
for i, v := range b {
o[i] = (o[i] - v) / normalize
}
} else {
for i, v := range b {
o[i] = (o[i] + v) / normalize
}
}
return o, nil
}

+ 27
- 0
synth/doc.go View File

@ -0,0 +1,27 @@
/*
Package synth provides functions for audio synthesis.
Stream
Most functions return a Stream, which is a contained channel that is
closable, so consumers may end the signal generation. Typical stream
usage may look like:
// Function that returns a stream
stream := synth.Func(...)
// Process frames from the stream
for i := 0; i < 128; i++ {
processSample(stream.Next())
}
// We're done here
stream.Close()
References
Mostly based in audiolazy © Copyright 2012-2016 Danilo de Jesus da Silva Bellini.
https://github.com/danilobellini/audiolazy
*/
package synth

+ 249
- 0
synth/line.go View File

@ -0,0 +1,249 @@
package synth
import (
gomath "math"
"time"
"maze.io/dsp.v0/math"
)
// Line produces a streamar finite stream with a straight stream.
func Line(duration int64, begin, end float64) *Stream {
step := (end - begin) / float64(duration)
stream := NewStream(1)
go func(duration int64, begin, step float64, stream *Stream) {
defer stream.Close()
for i := int64(0); i < duration; i++ {
next := begin + float64(i)*step
select {
case <-stream.stop:
return
case stream.stream <- next:
}
}
}(duration, begin, step, stream)
return stream
}
func FadeIn(duration int64) *Stream {
return Line(duration, 0, 1)
}
func FadeOut(duration int64) *Stream {
return Line(duration, 1, 0)
}
// Fixed returns a fixed stream of samples.
func Fixed(duration int64, value float64) *Stream {
stream := NewStream(1)
go func(d int64, v float64, stream *Stream) {
defer stream.Close()
for ; d >= 0; d-- {
select {
case <-stream.stop:
return
case stream.stream <- v:
}
}
}(duration, value, stream)
return stream
}
// FixedOnes stream.
func FixedOnes(duration int64) *Stream {
return Fixed(duration, 1)
}
// FixedZeros stream.
func FixedZeros(duration int64) *Stream {
return Fixed(duration, 0)
}
// LinearAttack is a streamar ADS fading attack generator.
func LinearAttack(attack, decay int64, sustain float64) *Stream {
// Attack and decay streams
a := 1.0 / float64(attack)
d := (sustain - 1) / float64(decay)
stream := NewStream(1)
go func(a, d, s float64, stream *Stream) {
defer stream.Close()
// attack
for i := 0.0; i < a; i++ {
v := i * a
select {
case <-stream.stop:
return
case stream.stream <- v:
}
}
// delay
for i := 0.0; i < d; i++ {
v := 1 + 1*d
select {
case <-stream.stop:
return
case stream.stream <- v:
}
}
// sustain
for {
select {
case <-stream.stop:
return
case stream.stream <- s:
}
}
}(a, d, sustain, stream)
return stream
}
// LinearADSR is a streamar ADSR envelope
func LinearADSR(duration, attack, decay int64, sustain float64, release int64) *Stream {
stream := NewStream(1)
go func(l int64, a, d, s, r float64, stream *Stream) {
defer stream.Close()
var (
i float64
ma = 1 / a
md = (s - 1) / d
mr = -s * 1 / r
ls = float64(l) - a - d - r
)
// attack
for i = 0; i < a; i++ {
v := i * ma
select {
case <-stream.stop:
return
case stream.stream <- v:
}
}
// decay
for i = 0; i < d; i++ {
v := 1 + i*md
select {
case <-stream.stop:
return
case stream.stream <- v:
}
}
// sustain
for i = 0; i < ls; i++ {
select {
case <-stream.stop:
return
case stream.stream <- s:
}
}
// release
for i = 0; i < r; i++ {
v := s + i*mr
select {
case <-stream.stop:
return
case stream.stream <- v:
}
}
}(duration, float64(attack), float64(decay), sustain, float64(release), stream)
return stream
}
func WhiteNoise(duration int64, low, high float64) *Stream {
if low > high {
low, high = high, low
}
rand := math.NewUniformRandom(time.Now().UnixNano())
stream := NewStream(1)
if duration > 0 {
return loopn(duration, stream, func() float64 {
return rand.Float64Range(low, high)
})
}
return loop(stream, func() float64 {
return rand.Float64Range(low, high)
})
}
func GaussianNoise(duration int64, mu, sigma float64) *Stream {
rand := math.NewUniformRandom(time.Now().UnixNano())
stream := NewStream(1)
if duration > 0 {
return loopn(duration, stream, func() float64 {
return rand.Gaussian(mu, sigma)
})
}