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"