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"