File: waveout/bytes.go
   1 package main
   2 
   3 import (
   4     "encoding/binary"
   5     "fmt"
   6     "io"
   7     "math"
   8 )
   9 
  10 // aiff header format
  11 //
  12 // http://paulbourke.net/dataformats/audio/
  13 //
  14 // wav header format
  15 //
  16 // http://soundfile.sapp.org/doc/WaveFormat/
  17 // http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
  18 // https://docs.fileformat.com/audio/wav/
  19 
  20 const (
  21     // maxInt helps convert float64 values into int16 ones
  22     maxInt = 1<<15 - 1
  23 
  24     // wavIntPCM declares integer PCM sound-data in a wav header
  25     wavIntPCM = 1
  26 
  27     // wavFloatPCM declares floating-point PCM sound-data in a wav header
  28     wavFloatPCM = 3
  29 )
  30 
  31 // emitInt16LE writes a 16-bit signed integer in little-endian byte order
  32 func emitInt16LE(w io.Writer, f float64) {
  33     // binary.Write(w, binary.LittleEndian, int16(maxInt*f))
  34     var buf [2]byte
  35     binary.LittleEndian.PutUint16(buf[:2], uint16(int16(maxInt*f)))
  36     w.Write(buf[:2])
  37 }
  38 
  39 // emitFloat32LE writes a 32-bit float in little-endian byte order
  40 func emitFloat32LE(w io.Writer, f float64) {
  41     var buf [4]byte
  42     binary.LittleEndian.PutUint32(buf[:4], math.Float32bits(float32(f)))
  43     w.Write(buf[:4])
  44 }
  45 
  46 // emitInt16BE writes a 16-bit signed integer in big-endian byte order
  47 func emitInt16BE(w io.Writer, f float64) {
  48     // binary.Write(w, binary.BigEndian, int16(maxInt*f))
  49     var buf [2]byte
  50     binary.BigEndian.PutUint16(buf[:2], uint16(int16(maxInt*f)))
  51     w.Write(buf[:2])
  52 }
  53 
  54 // emitFloat32BE writes a 32-bit float in big-endian byte order
  55 func emitFloat32BE(w io.Writer, f float64) {
  56     var buf [4]byte
  57     binary.BigEndian.PutUint32(buf[:4], math.Float32bits(float32(f)))
  58     w.Write(buf[:4])
  59 }
  60 
  61 // wavSettings is an item in the type2wavSettings table
  62 type wavSettings struct {
  63     Type          byte
  64     BitsPerSample byte
  65 }
  66 
  67 // type2wavSettings encodes values used when emitting wav headers
  68 var type2wavSettings = map[sampleFormat]wavSettings{
  69     int16LE:   {wavIntPCM, 16},
  70     float32LE: {wavFloatPCM, 32},
  71 }
  72 
  73 // emitWaveHeader writes the start of a valid .wav file: since it also starts
  74 // the wav data section and emits its size, you only need to write all samples
  75 // after calling this func
  76 func emitWaveHeader(w io.Writer, cfg outputConfig) error {
  77     const fmtChunkSize = 16
  78     duration := cfg.MaxTime
  79     numchan := uint32(cfg.NumChannels)
  80     sampleRate := cfg.SampleRate
  81 
  82     ws, ok := type2wavSettings[cfg.Samples]
  83     if !ok {
  84         const fs = `internal error: invalid output-format code %d`
  85         return fmt.Errorf(fs, cfg.Samples)
  86     }
  87     kind := uint16(ws.Type)
  88     bps := uint32(ws.BitsPerSample)
  89 
  90     // byte rate
  91     br := sampleRate * bps * numchan / 8
  92     // data size in bytes
  93     dataSize := uint32(float64(br) * duration)
  94     // total file size
  95     totalSize := uint32(dataSize + 44)
  96 
  97     // general descriptor
  98     w.Write([]byte(`RIFF`))
  99     binary.Write(w, binary.LittleEndian, uint32(totalSize))
 100     w.Write([]byte(`WAVE`))
 101 
 102     // fmt chunk
 103     w.Write([]byte(`fmt `))
 104     binary.Write(w, binary.LittleEndian, uint32(fmtChunkSize))
 105     binary.Write(w, binary.LittleEndian, uint16(kind))
 106     binary.Write(w, binary.LittleEndian, uint16(numchan))
 107     binary.Write(w, binary.LittleEndian, uint32(sampleRate))
 108     binary.Write(w, binary.LittleEndian, uint32(br))
 109     binary.Write(w, binary.LittleEndian, uint16(bps*numchan/8))
 110     binary.Write(w, binary.LittleEndian, uint16(bps))
 111 
 112     // start data chunk
 113     w.Write([]byte(`data`))
 114     binary.Write(w, binary.LittleEndian, uint32(dataSize))
 115     return nil
 116 }

     File: waveout/config.go
   1 package main
   2 
   3 import (
   4     "errors"
   5     "fmt"
   6     "math"
   7     "os"
   8     "strconv"
   9     "strings"
  10     "time"
  11 
  12     "../../pkg/timeplus"
  13 )
  14 
  15 // config has all the parsed cmd-line options
  16 type config struct {
  17     // LeftScript is the source code of the script for the left channel,
  18     // which may be the only channel when in mono mode
  19     LeftScript string
  20 
  21     // RightScript is the source code of the script for the right channel
  22     RightScript string
  23 
  24     // To is the output format
  25     To string
  26 
  27     // MaxTime is the play duration of the resulting sound
  28     MaxTime float64
  29 
  30     // SampleRate is the number of samples per second for all channels
  31     SampleRate uint
  32 
  33     // Stereo forces a single formula into stereo mode
  34     Stereo bool
  35 }
  36 
  37 // parseFlags is the constructor for type config
  38 func parseFlags(usage string) (config, error) {
  39     cfg := config{
  40         To:         `wav`,
  41         MaxTime:    1.0,
  42         SampleRate: 48_000,
  43     }
  44 
  45     args := os.Args[1:]
  46     if len(args) == 0 {
  47         fmt.Fprint(os.Stderr, usage)
  48         os.Exit(0)
  49     }
  50 
  51     for _, s := range args {
  52         switch s {
  53         case `help`, `-h`, `--h`, `-help`, `--help`:
  54             fmt.Fprint(os.Stderr, usage)
  55             os.Exit(0)
  56         }
  57 
  58         err := cfg.handleArg(s)
  59         if err != nil {
  60             return cfg, err
  61         }
  62     }
  63 
  64     if cfg.MaxTime < 0 {
  65         const fs = `error: given negative duration %f`
  66         return cfg, fmt.Errorf(fs, cfg.MaxTime)
  67     }
  68     return cfg, nil
  69 }
  70 
  71 func (c *config) handleArg(s string) error {
  72     switch s {
  73     case `44.1k`, `44.1K`:
  74         c.SampleRate = 44_100
  75         return nil
  76 
  77     case `48k`, `48K`:
  78         c.SampleRate = 48_000
  79         return nil
  80 
  81     case `stereo`, `STEREO`:
  82         c.Stereo = true
  83         return nil
  84 
  85     case `dat`, `DAT`:
  86         c.Stereo = true
  87         c.SampleRate = 48_000
  88         return nil
  89 
  90     case `cd`, `cda`, `CD`, `CDA`:
  91         c.Stereo = true
  92         c.SampleRate = 44_100
  93         return nil
  94     }
  95 
  96     // handle output-format names and their aliases
  97     if kind, ok := name2type[s]; ok {
  98         c.To = kind
  99         return nil
 100     }
 101 
 102     // handle time formats, except when they're pure numbers
 103     dur, derr := timeplus.ParseDuration(s)
 104     f, ferr := strconv.ParseFloat(s, 64)
 105     if derr == nil && ferr != nil && !isBadNumber(f) {
 106         c.MaxTime = float64(dur) / float64(time.Second)
 107         return nil
 108     }
 109 
 110     // handle sample-rate, given either in hertz or kilohertz
 111     lc := strings.ToLower(s)
 112     if strings.HasSuffix(lc, `khz`) {
 113         lc = strings.TrimSuffix(lc, `khz`)
 114         khz, err := strconv.ParseFloat(lc, 64)
 115         if err != nil || isBadNumber(khz) || khz <= 0 {
 116             const fs = `invalid sample-rate frequency %q`
 117             return fmt.Errorf(fs, s)
 118         }
 119         c.SampleRate = uint(1_000 * khz)
 120         return nil
 121     } else if strings.HasSuffix(lc, `hz`) {
 122         lc = strings.TrimSuffix(lc, `hz`)
 123         hz, err := strconv.ParseUint(lc, 10, 64)
 124         if err != nil {
 125             const fs = `invalid sample-rate frequency %q`
 126             return fmt.Errorf(fs, s)
 127         }
 128         c.SampleRate = uint(hz)
 129         return nil
 130     }
 131 
 132     // handle formulas
 133     if len(c.LeftScript) > 0 && len(c.RightScript) > 0 {
 134         const msg = `expected only 1 or 2 formulas, but got more`
 135         return errors.New(msg)
 136     }
 137 
 138     if len(c.LeftScript) == 0 {
 139         c.LeftScript = s
 140     } else {
 141         c.RightScript = s
 142     }
 143     return nil
 144 }
 145 
 146 type encoding byte
 147 type headerType byte
 148 type sampleFormat byte
 149 
 150 const (
 151     directEncoding encoding = 1
 152     uriEncoding    encoding = 2
 153 
 154     noHeader  headerType = 1
 155     wavHeader headerType = 2
 156 
 157     int16BE   sampleFormat = 1
 158     int16LE   sampleFormat = 2
 159     float32BE sampleFormat = 3
 160     float32LE sampleFormat = 4
 161 )
 162 
 163 // name2type normalizes keys used for type2settings
 164 var name2type = map[string]string{
 165     `datauri`:  `data-uri`,
 166     `dataurl`:  `data-uri`,
 167     `data-uri`: `data-uri`,
 168     `data-url`: `data-uri`,
 169     `uri`:      `data-uri`,
 170     `url`:      `data-uri`,
 171 
 172     `raw`:     `raw`,
 173     `raw16be`: `raw16be`,
 174     `raw16le`: `raw16le`,
 175     `raw32be`: `raw32be`,
 176     `raw32le`: `raw32le`,
 177 
 178     `audio/x-wav`:  `wave-16`,
 179     `audio/x-wave`: `wave-16`,
 180     `wav`:          `wave-16`,
 181     `wave`:         `wave-16`,
 182     `wav16`:        `wave-16`,
 183     `wave16`:       `wave-16`,
 184     `wav-16`:       `wave-16`,
 185     `wave-16`:      `wave-16`,
 186     `x-wav`:        `wave-16`,
 187     `x-wave`:       `wave-16`,
 188 
 189     `wav16uri`:    `wave-16-uri`,
 190     `wave-16-uri`: `wave-16-uri`,
 191 
 192     `wav32uri`:    `wave-32-uri`,
 193     `wave-32-uri`: `wave-32-uri`,
 194 
 195     `wav32`:   `wave-32`,
 196     `wave32`:  `wave-32`,
 197     `wav-32`:  `wave-32`,
 198     `wave-32`: `wave-32`,
 199 }
 200 
 201 // outputSettings are format-specific settings which are controlled by the
 202 // output-format option on the cmd-line
 203 type outputSettings struct {
 204     Encoding encoding
 205     Header   headerType
 206     Samples  sampleFormat
 207 }
 208 
 209 // type2settings translates output-format names into the specific settings
 210 // these imply
 211 var type2settings = map[string]outputSettings{
 212     ``: {directEncoding, wavHeader, int16LE},
 213 
 214     `data-uri`:    {uriEncoding, wavHeader, int16LE},
 215     `raw`:         {directEncoding, noHeader, int16LE},
 216     `raw16be`:     {directEncoding, noHeader, int16BE},
 217     `raw16le`:     {directEncoding, noHeader, int16LE},
 218     `wave-16`:     {directEncoding, wavHeader, int16LE},
 219     `wave-16-uri`: {uriEncoding, wavHeader, int16LE},
 220 
 221     `raw32be`:     {directEncoding, noHeader, float32BE},
 222     `raw32le`:     {directEncoding, noHeader, float32LE},
 223     `wave-32`:     {directEncoding, wavHeader, float32LE},
 224     `wave-32-uri`: {uriEncoding, wavHeader, float32LE},
 225 }
 226 
 227 // outputConfig has all the info the core of this app needs to make sound
 228 type outputConfig struct {
 229     // Left is the source code of the script for the left channel,
 230     // which may be the only channel when in mono mode
 231     Left string
 232 
 233     // Right is the source code of the script for the right channel
 234     Right string
 235 
 236     // MaxTime is the play duration of the resulting sound
 237     MaxTime float64
 238 
 239     // SampleRate is the number of samples per second for all channels
 240     SampleRate uint32
 241 
 242     // all the configuration details needed to emit output
 243     outputSettings
 244 
 245     // NumChannels is the number of output channels, either 1 or 2
 246     NumChannels byte
 247 }
 248 
 249 // newOutputConfig is the constructor for type outputConfig, translating the
 250 // cmd-line info from type config
 251 func newOutputConfig(cfg config) (outputConfig, error) {
 252     oc := outputConfig{
 253         Left:    cfg.LeftScript,
 254         Right:   cfg.RightScript,
 255         MaxTime: cfg.MaxTime,
 256 
 257         SampleRate:  uint32(cfg.SampleRate),
 258         NumChannels: 0,
 259     }
 260 
 261     if len(oc.Left) == 0 && len(oc.Right) == 0 {
 262         const msg = `no formulas given`
 263         return oc, errors.New(msg)
 264     }
 265 
 266     if cfg.Stereo {
 267         oc.NumChannels = 2
 268     } else {
 269         oc.NumChannels = 0
 270         if len(oc.Left) > 0 {
 271             oc.NumChannels++
 272         }
 273         if len(oc.Right) > 0 {
 274             oc.NumChannels++
 275         }
 276     }
 277 
 278     if oc.NumChannels == 2 && len(oc.Right) == 0 {
 279         oc.Right = oc.Left
 280     }
 281 
 282     outFmt := strings.ToLower(strings.TrimSpace(cfg.To))
 283     if alias, ok := name2type[outFmt]; ok {
 284         outFmt = alias
 285     }
 286 
 287     set, ok := type2settings[outFmt]
 288     if !ok {
 289         const fs = `unsupported output format %q`
 290         return oc, fmt.Errorf(fs, cfg.To)
 291     }
 292 
 293     oc.outputSettings = set
 294     return oc, nil
 295 }
 296 
 297 // mimeType gives the format's corresponding MIME type, or an empty string
 298 // if the type isn't URI-encodable
 299 func (oc outputConfig) mimeType() string {
 300     if oc.Header == wavHeader {
 301         return `audio/x-wav`
 302     }
 303     return ``
 304 }
 305 
 306 func isBadNumber(f float64) bool {
 307     return math.IsNaN(f) || math.IsInf(f, 0)
 308 }

     File: waveout/config_test.go
   1 package main
   2 
   3 import "testing"
   4 
   5 func TestTables(t *testing.T) {
   6     for _, kind := range name2type {
   7         // ensure all canonical format values are aliased to themselves
   8         if _, ok := name2type[kind]; !ok {
   9             const fs = `canonical format %q not set`
  10             t.Fatalf(fs, kind)
  11         }
  12     }
  13 
  14     for k, kind := range name2type {
  15         // ensure each setting leads somewhere
  16         set, ok := type2settings[kind]
  17         if !ok {
  18             const fs = `type alias %q has no setting for it`
  19             t.Fatalf(fs, k)
  20         }
  21 
  22         // ensure all encoding codes are valid in the next step
  23         switch set.Encoding {
  24         case directEncoding, uriEncoding:
  25             // ok
  26         default:
  27             const fs = `invalid encoding (code %d) from settings for %q`
  28             t.Fatalf(fs, set.Encoding, kind)
  29         }
  30 
  31         // also ensure all header codes are valid
  32         switch set.Header {
  33         case noHeader, wavHeader:
  34             // ok
  35         default:
  36             const fs = `invalid header (code %d) from settings for %q`
  37             t.Fatalf(fs, set.Header, kind)
  38         }
  39 
  40         // as well as all sample-format codes
  41         switch set.Samples {
  42         case int16BE, int16LE, float32BE, float32LE:
  43             // ok
  44         default:
  45             const fs = `invalid sample-format (code %d) from settings for %q`
  46             t.Fatalf(fs, set.Header, kind)
  47         }
  48     }
  49 }

     File: waveout/info.txt
   1 waveout [options...] [formulas...]
   2 
   3 
   4 This app emits wave-sound binary data using the script(s) given. Scripts
   5 give you the float64-related functionality you may expect, from numeric
   6 operations to several math functions. When given 1 formula, the result is
   7 mono; when given 2 formulas (left and right), the result is stereo. When
   8 given `stereo` as an option on its own, even 1 formula will emit stereo.
   9 
  10 Output is always uncompressed audio: `waveout` can emit that as is, or as a
  11 base64-encoded data-URI, which you can use as a `src` attribute value in an
  12 HTML audio tag. Output duration is 1 second by default, but you can change
  13 that too by using a recognized time format.
  14 
  15 The first recognized time format is the familiar hh:mm:ss, where the hours
  16 are optional, and where seconds can have a decimal part after it.
  17 
  18 The second recognized time format uses 1-letter shortcuts instead of colons
  19 for each time component, each of which is optional: `h` stands for hour, `m`
  20 for minutes, and `s` for seconds.
  21 
  22 
  23 Output Formats
  24 
  25              encoding  header  samples  endian   more info
  26 
  27     wav      direct    wave    int16    little   default format
  28 
  29     wav16    direct    wave    int16    little   alias for `wav`
  30     wav32    direct    wave    float32  little
  31     uri      data-URI  wave    int16    little   MIME type is audio/x-wav
  32 
  33     raw      direct    none    int16    little
  34     raw16le  direct    none    int16    little   alias for `raw`
  35     raw32le  direct    none    float32  little
  36     raw16be  direct    none    int16    big
  37     raw32be  direct    none    float32  big
  38 
  39 
  40 Concrete Examples
  41 
  42 # low-tones commonly used in club music as beats
  43 waveout 2s 'sin(10 * tau * exp(-20 * u)) * exp(-2 * u)' > club-beats.wav
  44 
  45 # 1 minute and 5 seconds of static-like random noise
  46 waveout 1m5s 'rand()' > random-noise.wav
  47 
  48 # many bell-like clicks in quick succession; can be a cellphone's ringtone
  49 waveout 'sin(2048 * tau * t) * exp(-50 * (t%0.1))' > ringtone.wav
  50 
  51 # similar to the door-opening sound from a dsc powerseries home alarm
  52 waveout 'sin(4096 * tau * t) * exp(-10 * (t%0.1))' > home-alarm.wav
  53 
  54 # watch your ears: quickly increases frequency up to 2khz
  55 waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav
  56 
  57 # 1-second 400hz test tone
  58 waveout 'sin(400 * tau * t)' > test-tone-400.wav
  59 
  60 # 2s of a 440hz test tone, also called an A440 sound
  61 waveout 2s 'sin(440 * tau * t)' > a440.wav
  62 
  63 # 1s 400hz test tone with sudden volume drop at the end, to avoid clip
  64 waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav
  65 
  66 # old ringtone used in north america
  67 waveout '0.5*sin(350 * tau * t) + 0.5*sin(450 * tau * t)' > na-ringtone.wav
  68 
  69 # 20 seconds of periodic pings
  70 waveout 20s 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav
  71 
  72 # 2 seconds of a european-style dial-tone
  73 waveout 2s '(sin(350 * tau * t) + sin(450 * tau * t)) / 2' > dial-tone.wav
  74 
  75 # 4 seconds of a north-american-style busy-phone signal
  76 waveout 4s '(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' > na-busy.wav
  77 
  78 # hit the 51st key on a synthetic piano-like instrument
  79 waveout 'sin(tau * 440 * 2**((51 - 49)/12) * t) * exp(-10*u)' > piano-key.wav
  80 
  81 # hit of a synthetic snare-like sound
  82 waveout 'random() * exp(-10 * t)' > synth-snare.wav
  83 
  84 # a stereotypical `laser` sound
  85 waveout 'sin(100 * tau * exp(-40 * t))' > laser.wav

     File: waveout/logo.ico   <BINARY>

     File: waveout/logo.png   <BINARY>

     File: waveout/main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "encoding/base64"
   6     "errors"
   7     "fmt"
   8     "io"
   9     "os"
  10 
  11     _ "embed"
  12 )
  13 
  14 //go:embed info.txt
  15 var usage string
  16 
  17 func main() {
  18     cfg, err := parseFlags(usage)
  19     if err != nil {
  20         fmt.Fprintln(os.Stderr, err.Error())
  21         os.Exit(1)
  22     }
  23 
  24     oc, err := newOutputConfig(cfg)
  25     if err != nil {
  26         fmt.Fprintln(os.Stderr, err.Error())
  27         os.Exit(1)
  28     }
  29 
  30     addDetermFuncs()
  31 
  32     if err := run(oc); err != nil {
  33         fmt.Fprintln(os.Stderr, err.Error())
  34         os.Exit(1)
  35     }
  36 }
  37 
  38 func run(cfg outputConfig) error {
  39     // f, err := os.Create(`waveout.prof`)
  40     // if err != nil {
  41     //  return err
  42     // }
  43     // defer f.Close()
  44 
  45     // pprof.StartCPUProfile(f)
  46     // defer pprof.StopCPUProfile()
  47 
  48     w := bufio.NewWriterSize(os.Stdout, 64*1024)
  49     defer w.Flush()
  50 
  51     switch cfg.Encoding {
  52     case directEncoding:
  53         return runDirect(w, cfg)
  54 
  55     case uriEncoding:
  56         mtype := cfg.mimeType()
  57         if mtype == `` {
  58             return errors.New(`internal error: no MIME type`)
  59         }
  60 
  61         fmt.Fprintf(w, `data:%s;base64,`, mtype)
  62         enc := base64.NewEncoder(base64.StdEncoding, w)
  63         defer enc.Close()
  64         return runDirect(enc, cfg)
  65 
  66     default:
  67         const fs = `internal error: wrong output-encoding code %d`
  68         return fmt.Errorf(fs, cfg.Encoding)
  69     }
  70 }
  71 
  72 // type2emitter chooses sample-emitter funcs from the format given
  73 var type2emitter = map[sampleFormat]func(io.Writer, float64){
  74     int16LE:   emitInt16LE,
  75     int16BE:   emitInt16BE,
  76     float32LE: emitFloat32LE,
  77     float32BE: emitFloat32BE,
  78 }
  79 
  80 // runDirect emits sound-data bytes: this func can be called with writers
  81 // which keep bytes as given, or with re-encoders, such as base64 writers
  82 func runDirect(w io.Writer, cfg outputConfig) error {
  83     switch cfg.Header {
  84     case noHeader:
  85         // do nothing, while avoiding error
  86 
  87     case wavHeader:
  88         emitWaveHeader(w, cfg)
  89 
  90     default:
  91         const fs = `internal error: wrong header code %d`
  92         return fmt.Errorf(fs, cfg.Header)
  93     }
  94 
  95     emit, ok := type2emitter[cfg.Samples]
  96     if !ok {
  97         const fs = `internal error: wrong output-format code %d`
  98         return fmt.Errorf(fs, cfg.Samples)
  99     }
 100 
 101     switch cfg.NumChannels {
 102     case 1:
 103         return emitMono(w, cfg, emit)
 104     case 2:
 105         return emitStereo(w, cfg, emit)
 106     default:
 107         const fs = `internal error: can only emit 1 or 2 sound channels, not %d`
 108         return fmt.Errorf(fs, cfg.NumChannels)
 109     }
 110 }

     File: waveout/save-all-examples.sh
   1 #!/bin/sh
   2 
   3 # low-tones commonly used in club music as beats
   4 waveout -time 2 'sin(10 * tau * exp(-20 * u)) * exp(-2 * u)' > club-beats.wav
   5 
   6 # 1 minute and 5 seconds of static-like random noise
   7 waveout -time=1m5s 'rand()' > random-noise.wav
   8 
   9 # many bell-like clicks in quick succession; can be a cellphone's ringtone
  10 waveout 'sin(2048 * tau * t) * exp(-50 * (t%0.1))' > ringtone.wav
  11 
  12 # watch your ears: quickly increases frequency up to 2khz
  13 waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav
  14 
  15 # 1-second 400hz test tone
  16 waveout 'sin(400 * tau * t)' > test-tone-400.wav
  17 
  18 # 1s 400hz test tone with sudden volume drop at the end, to avoid clip
  19 waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav
  20 
  21 # 20 seconds of periodic pings
  22 waveout -time 20 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav

     File: waveout/scripts.go
   1 package main
   2 
   3 import (
   4     "io"
   5     "math"
   6     "math/rand"
   7     "time"
   8 
   9     "../../pkg/fmscripts"
  10 )
  11 
  12 // makeDefs makes extra funcs and values available to scripts
  13 func makeDefs(cfg outputConfig) map[string]any {
  14     // copy extra built-in funcs
  15     defs := make(map[string]any, len(extras)+6+5)
  16     for k, v := range extras {
  17         defs[k] = v
  18     }
  19 
  20     // add extra variables
  21     defs[`t`] = 0.0
  22     defs[`u`] = 0.0
  23     defs[`d`] = cfg.MaxTime
  24     defs[`dur`] = cfg.MaxTime
  25     defs[`duration`] = cfg.MaxTime
  26     defs[`end`] = cfg.MaxTime
  27 
  28     // add pseudo-random funcs
  29 
  30     seed := time.Now().UnixNano()
  31     r := rand.New(rand.NewSource(seed))
  32 
  33     rand := func() float64 {
  34         return random01(r)
  35     }
  36     randomf := func() float64 {
  37         return random(r)
  38     }
  39     rexpf := func(scale float64) float64 {
  40         return rexp(r, scale)
  41     }
  42     rnormf := func(mu, sigma float64) float64 {
  43         return rnorm(r, mu, sigma)
  44     }
  45 
  46     defs[`rand`] = rand
  47     defs[`rand01`] = rand
  48     defs[`random`] = randomf
  49     defs[`rexp`] = rexpf
  50     defs[`rnorm`] = rnormf
  51 
  52     return defs
  53 }
  54 
  55 type emitFunc = func(io.Writer, float64)
  56 
  57 // emitMono runs the formula given to emit all single-channel wave samples
  58 func emitMono(w io.Writer, cfg outputConfig, emit emitFunc) error {
  59     var c fmscripts.Compiler
  60     mono, err := c.Compile(cfg.Left, makeDefs(cfg))
  61     if err != nil {
  62         return err
  63     }
  64 
  65     t, _ := mono.Get(`t`)
  66     u, needsu := mono.Get(`u`)
  67 
  68     dt := 1.0 / float64(cfg.SampleRate)
  69     end := cfg.MaxTime
  70 
  71     // update variable `u` only if script uses it: this can speed things
  72     // up considerably when that variable isn't used
  73     updateu := func(float64) {}
  74     if needsu {
  75         updateu = func(now float64) { _, *u = math.Modf(now) }
  76     }
  77 
  78     for i := 0.0; true; i++ {
  79         now := dt * i
  80         if now >= end {
  81             return nil
  82         }
  83 
  84         *t = now
  85         updateu(now)
  86         emit(w, mono.Run())
  87     }
  88     return nil
  89 }
  90 
  91 // emitStereo runs the formula given to emit all 2-channel wave samples
  92 func emitStereo(w io.Writer, cfg outputConfig, emit emitFunc) error {
  93     defs := makeDefs(cfg)
  94     var c fmscripts.Compiler
  95     left, err := c.Compile(cfg.Left, defs)
  96     if err != nil {
  97         return err
  98     }
  99     right, err := c.Compile(cfg.Right, defs)
 100     if err != nil {
 101         return err
 102     }
 103 
 104     lt, _ := left.Get(`t`)
 105     rt, _ := right.Get(`t`)
 106     lu, luok := left.Get(`u`)
 107     ru, ruok := right.Get(`u`)
 108 
 109     dt := 1.0 / float64(cfg.SampleRate)
 110     end := cfg.MaxTime
 111 
 112     // update variable `u` only if script uses it: this can speed things
 113     // up considerably when that variable isn't used
 114     updateu := func(float64) {}
 115     if luok || ruok {
 116         updateu = func(now float64) {
 117             _, u := math.Modf(now)
 118             *lu = u
 119             *ru = u
 120         }
 121     }
 122 
 123     for i := 0.0; true; i++ {
 124         now := dt * i
 125         if now >= end {
 126             return nil
 127         }
 128 
 129         *rt = now
 130         *lt = now
 131         updateu(now)
 132 
 133         // most software seems to emit stereo pairs in left-right order
 134         emit(w, left.Run())
 135         emit(w, right.Run())
 136     }
 137 
 138     // keep the compiler happy
 139     return nil
 140 }

     File: waveout/stdlib.go
   1 package main
   2 
   3 import (
   4     "math"
   5     "math/rand"
   6 
   7     "../../pkg/fmscripts"
   8     "../../pkg/mathplus"
   9 )
  10 
  11 // tau is exactly 1 loop around a circle, which is handy to turn frequencies
  12 // into trigonometric angles, since they're measured in radians
  13 const tau = 2 * math.Pi
  14 
  15 // extras has funcs beyond what the script built-ins offer: those built-ins
  16 // are for general math calculations, while these are specific for sound
  17 // effects, other sound-related calculations, or to make pseudo-random values
  18 var extras = map[string]any{
  19     `hihat`: hihat,
  20 }
  21 
  22 // addDetermFuncs does what it says, ensuring these funcs are optimizable when
  23 // they're given all-constant expressions as inputs
  24 func addDetermFuncs() {
  25     fmscripts.DefineDetFuncs(map[string]any{
  26         `ascale`:       mathplus.AnchoredScale,
  27         `awrap`:        mathplus.AnchoredWrap,
  28         `clamp`:        mathplus.Clamp,
  29         `epa`:          mathplus.Epanechnikov,
  30         `epanechnikov`: mathplus.Epanechnikov,
  31         `fract`:        mathplus.Fract,
  32         `gauss`:        mathplus.Gauss,
  33         `horner`:       mathplus.Polyval,
  34         `logistic`:     mathplus.Logistic,
  35         `mix`:          mathplus.Mix,
  36         `polyval`:      mathplus.Polyval,
  37         `scale`:        mathplus.Scale,
  38         `sign`:         mathplus.Sign,
  39         `sinc`:         mathplus.Sinc,
  40         `smoothstep`:   mathplus.SmoothStep,
  41         `step`:         mathplus.Step,
  42         `tricube`:      mathplus.Tricube,
  43         `unwrap`:       mathplus.Unwrap,
  44         `wrap`:         mathplus.Wrap,
  45 
  46         `drop`:       dropsince,
  47         `dropfrom`:   dropsince,
  48         `dropoff`:    dropsince,
  49         `dropsince`:  dropsince,
  50         `kick`:       kick,
  51         `kicklow`:    kicklow,
  52         `piano`:      piano,
  53         `pianokey`:   piano,
  54         `pickval`:    pickval,
  55         `pickvalue`:  pickval,
  56         `sched`:      schedule,
  57         `schedule`:   schedule,
  58         `timeval`:    timeval,
  59         `timevalues`: timeval,
  60     })
  61 }
  62 
  63 // random01 returns a random value in 0 .. 1
  64 func random01(r *rand.Rand) float64 {
  65     return r.Float64()
  66 }
  67 
  68 // random returns a random value in -1 .. +1
  69 func random(r *rand.Rand) float64 {
  70     return (2 * r.Float64()) - 1
  71 }
  72 
  73 // rexp returns an exponentially-distributed random value using the scale
  74 // (expected value) given
  75 func rexp(r *rand.Rand, scale float64) float64 {
  76     return scale * r.ExpFloat64()
  77 }
  78 
  79 // rnorm returns a normally-distributed random value using the mean and
  80 // standard deviation given
  81 func rnorm(r *rand.Rand, mu, sigma float64) float64 {
  82     return r.NormFloat64()*sigma + mu
  83 }
  84 
  85 // make sample for a synthetic-drum kick
  86 func kick(t float64, f, k float64) float64 {
  87     const p = 0.085
  88     return math.Sin(tau*f*math.Pow(p, t)) * math.Exp(-k*t)
  89 }
  90 
  91 // make sample for a heavier-sounding synthetic-drum kick
  92 func kicklow(t float64, f, k float64) float64 {
  93     const p = 0.08
  94     return math.Sin(tau*f*math.Pow(p, t)) * math.Exp(-k*t)
  95 }
  96 
  97 // make sample for a synthetic hi-hat hit
  98 func hihat(t float64, k float64) float64 {
  99     return rand.Float64() * math.Exp(-k*t)
 100 }
 101 
 102 // schedule rearranges time, without being a time machine
 103 func schedule(t float64, period, delay float64) float64 {
 104     v := t + (1 - delay)
 105     if v < 0 {
 106         return 0
 107     }
 108     return math.Mod(v*period, period)
 109 }
 110 
 111 // make sample for a synthetic piano key being hit
 112 func piano(t float64, n float64) float64 {
 113     p := (math.Floor(n) - 49) / 12
 114     f := 440 * math.Pow(2, p)
 115     return math.Sin(tau * f * t)
 116 }
 117 
 118 // multiply rest of a formula with this for a quick volume drop at the end:
 119 // this is handy to avoid clips when sounds end playing
 120 func dropsince(t float64, start float64) float64 {
 121     // return math.Min(1, math.Exp(-100*(t-start)))
 122     if t <= start {
 123         return 1
 124     }
 125     return math.Exp(-100 * (t - start))
 126 }
 127 
 128 // pickval requires at least 3 args, the first 2 being the current time and
 129 // each slot's duration, respectively: these 2 are followed by all the values
 130 // to pick for all time slots
 131 func pickval(args ...float64) float64 {
 132     if len(args) < 3 {
 133         return 0
 134     }
 135 
 136     t := args[0]
 137     slotdur := args[1]
 138     values := args[2:]
 139 
 140     u, _ := math.Modf(t / slotdur)
 141     n := len(values)
 142     i := int(u) % n
 143     if 0 <= i && i < n {
 144         return values[i]
 145     }
 146     return 0
 147 }
 148 
 149 // timeval requires at least 2 args, the first 2 being the current time and
 150 // the total looping-period, respectively: these 2 are followed by pairs of
 151 // numbers, each consisting of a timestamp and a matching value, in order
 152 func timeval(args ...float64) float64 {
 153     if len(args) < 2 {
 154         return 0
 155     }
 156 
 157     t := args[0]
 158     period := args[1]
 159     u, _ := math.Modf(t / period)
 160 
 161     // find the first value whose periodic timestamp is due
 162     for rest := args[2:]; len(rest) >= 2; rest = rest[2:] {
 163         if u >= rest[0]/period {
 164             return rest[1]
 165         }
 166     }
 167     return 0
 168 }

     File: waveout/winapp_amd64.syso   <BINARY>

     File: waveout/winapp.rc
   1 // https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
   2 // windres -o winapp_amd64.syso winapp.rc
   3 
   4 IDI_ICON1 ICON "logo.ico"