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

     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...] [duration...] [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, and so
   8 on.
   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     emitter, 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     if len(cfg.Scripts) == 1 {
 102         return emitMono(w, cfg, emitter)
 103     }
 104     return emit(w, cfg, emitter)
 105 }

     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 // emit runs the formulas given to emit all wave samples
  58 func emit(w io.Writer, cfg outputConfig, emit emitFunc) error {
  59     var c fmscripts.Compiler
  60     defs := makeDefs(cfg)
  61 
  62     programs := make([]fmscripts.Program, 0, len(cfg.Scripts))
  63     tvars := make([]*float64, 0, len(cfg.Scripts))
  64     uvars := make([]*float64, 0, len(cfg.Scripts))
  65 
  66     for _, s := range cfg.Scripts {
  67         p, err := c.Compile(s, defs)
  68         if err != nil {
  69             return err
  70         }
  71         programs = append(programs, p)
  72         t, _ := p.Get(`t`)
  73         u, _ := p.Get(`u`)
  74         tvars = append(tvars, t)
  75         uvars = append(uvars, u)
  76     }
  77 
  78     dt := 1.0 / float64(cfg.SampleRate)
  79     end := cfg.MaxTime
  80 
  81     for i := 0.0; true; i++ {
  82         now := dt * i
  83         if now >= end {
  84             return nil
  85         }
  86 
  87         _, u := math.Modf(now)
  88 
  89         for j, p := range programs {
  90             *tvars[j] = now
  91             *uvars[j] = u
  92             emit(w, p.Run())
  93         }
  94     }
  95     return nil
  96 }
  97 
  98 // emitMono runs the formula given to emit all single-channel wave samples
  99 func emitMono(w io.Writer, cfg outputConfig, emit emitFunc) error {
 100     var c fmscripts.Compiler
 101     mono, err := c.Compile(cfg.Scripts[0], makeDefs(cfg))
 102     if err != nil {
 103         return err
 104     }
 105 
 106     t, _ := mono.Get(`t`)
 107     u, needsu := mono.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     if needsu {
 115         for i := 0.0; true; i++ {
 116             now := dt * i
 117             if now >= end {
 118                 return nil
 119             }
 120 
 121             *t = now
 122             _, *u = math.Modf(now)
 123             emit(w, mono.Run())
 124         }
 125         return nil
 126     }
 127 
 128     for i := 0.0; true; i++ {
 129         now := dt * i
 130         if now >= end {
 131             return nil
 132         }
 133 
 134         *t = now
 135         emit(w, mono.Run())
 136     }
 137     return nil
 138 }
 139 
 140 // // emitStereo runs the formula given to emit all 2-channel wave samples
 141 // func emitStereo(w io.Writer, cfg outputConfig, emit emitFunc) error {
 142 //  defs := makeDefs(cfg)
 143 //  var c fmscripts.Compiler
 144 //  left, err := c.Compile(cfg.Scripts[0], defs)
 145 //  if err != nil {
 146 //      return err
 147 //  }
 148 //  right, err := c.Compile(cfg.Scripts[1], defs)
 149 //  if err != nil {
 150 //      return err
 151 //  }
 152 
 153 //  lt, _ := left.Get(`t`)
 154 //  rt, _ := right.Get(`t`)
 155 //  lu, luok := left.Get(`u`)
 156 //  ru, ruok := right.Get(`u`)
 157 
 158 //  dt := 1.0 / float64(cfg.SampleRate)
 159 //  end := cfg.MaxTime
 160 
 161 //  // update variable `u` only if script uses it: this can speed things
 162 //  // up considerably when that variable isn't used
 163 //  updateu := func(float64) {}
 164 //  if luok || ruok {
 165 //      updateu = func(now float64) {
 166 //          _, u := math.Modf(now)
 167 //          *lu = u
 168 //          *ru = u
 169 //      }
 170 //  }
 171 
 172 //  for i := 0.0; true; i++ {
 173 //      now := dt * i
 174 //      if now >= end {
 175 //          return nil
 176 //      }
 177 
 178 //      *rt = now
 179 //      *lt = now
 180 //      updateu(now)
 181 
 182 //      // most software seems to emit stereo pairs in left-right order
 183 //      emit(w, left.Run())
 184 //      emit(w, right.Run())
 185 //  }
 186 
 187 //  // keep the compiler happy
 188 //  return nil
 189 // }

     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"