util  Check-in [908662f1b1]

Overview
Comment:initial ostinata commit
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 908662f1b1236804a95053889be0fae1f515d53832afe4f7f5dcfc06e9ed1aff
User & Date: lexi on 2025-03-12 06:46:14
Other Links: manifest | tags
Context
2025-03-14
17:35
add some instruments, fix some bugs (piano is not real yet) check-in: 5422ffcfca user: lexi tags: trunk
2025-03-12
06:46
initial ostinata commit check-in: 908662f1b1 user: lexi tags: trunk
2024-07-10
19:11
ave julianus imperator check-in: e02ae59ffd user: lexi tags: trunk
Changes

Added ostinata/alg.janet version [ca434a4824].

            1  +# [ʞ] alg.janet
            2  +#  ~ lexi hale <lexi@hale.su>
            3  +#  🄯 AGPLv3
            4  +#  ? basic algorithms inexplicably missing from std
            5  +
            6  +(defn vect ``
            7  +	vectorize primitive operations
            8  +
            9  +		`(vect + [1 2 3] [3 2 1]) #→ [4 4 4]`
           10  +
           11  +		`(vect * [1 2  ] [2 2 5]) #→ [2 4 5]`
           12  +	`` [op & vecs]
           13  +
           14  +	(def a @[])
           15  +	(var col 0)
           16  +	(forever 
           17  +		# theoretically this should be as simple as ;(map idx v)
           18  +		# but we want to handle missing columns sanely, which is
           19  +		# more complicated.
           20  +		(def args
           21  +			(mapcat (fn [v]
           22  +						(if (>= col (length v)) []
           23  +							[(get v col)]))
           24  +					vecs))
           25  +		(when (empty? args) (break))
           26  +		(put a col (apply op args))
           27  +		(++ col))
           28  +	(freeze a))
           29  +
           30  +(defn clamp [v &opt m x]
           31  +	(min (or x 1) (max (or m 0) v)))
           32  +
           33  +(defn xlerp [t a b]
           34  +	  (+ (* a (- 1 t))
           35  +		 (* b      t)))
           36  +
           37  +(defn lerp [t a b]
           38  +	(xlerp (clamp t) a b))
           39  +
           40  +(defn sel "return a record filtered by a key predicate"
           41  +	[rec p]
           42  +	(def r @{})
           43  +	(eachp (k v) rec
           44  +		(when (p k) (put r k v)))
           45  +	(if (table? rec) r (freeze r)))
           46  +
           47  +(defn qrec "query a record by key names, returning a matching sub-record"
           48  +	[rec & ks]
           49  +	(def r @{})
           50  +	(each k ks
           51  +		(put r k (get rec k)))
           52  +	(if (table? rec) r (freeze r)))
           53  +
           54  +(defn llut "get the lerp halves for a linear lookup table"
           55  +	[t lut]
           56  +	(def t (clamp t))
           57  +	(case t
           58  +		0 [0 (first lut) (first lut)]
           59  +		1 [1 (last  lut) (last  lut)]
           60  +		(do (def q (- (length lut) 1))
           61  +			(def n (* q t))
           62  +			(def idx (math/trunc n))
           63  +			(def frac (- n idx))
           64  +			[frac
           65  +			 (lut      idx)
           66  +			 (lut (+ 1 idx))])))
           67  +
           68  +(defn ilut "interpolate a linear lookup table"
           69  +	[t lut]
           70  +	(lerp ;(llut t lut)))
           71  +
           72  +(defn vlut "interpolate a linear vector lookup table"
           73  +	[t lut]
           74  +	(def [fac a b] (llut t lut))
           75  +	(vect |(lerp fac ;$&) a b))
           76  +
           77  +(defn lut-bake "bake a lut from a function"
           78  +	[res f]
           79  +	(def t (array/ensure @[] res 1))
           80  +	(loop [i :range [0 res] :let [fac (/ i res)]]
           81  +		(put t i (f fac)))
           82  +	t)

Added ostinata/ostinata.janet version [ec4176d797].

            1  +# [ʞ] ostinata.janet
            2  +#  ~ lexi hale <lexi@hale.su>
            3  +#  🄯 AGPLv3
            4  +#  ? a TUI mini-garageband that uses csound as
            5  +#    a backend synthesizer. to add a new instrument,
            6  +#    write it in ['ostinata.orc], then add it to
            7  +#    [$orchestra] here
            8  +
            9  +(import /tui)
           10  +(import /alg)
           11  +
           12  +(def terminate (gensym))
           13  +(def teardown (gensym))
           14  +
           15  +(def grammar (peg/compile ~{
           16  +	:qlit (* `"` (% (any (+ (* `\"` (constant `"`))
           17  +	                        (* (not `"`) '1))))
           18  +	         `"`)
           19  +	:idchar (* (not :s) '1)
           20  +	:term (% (some (+ :qlit :idchar)))
           21  +	:seq (* :s* :term (? (* :s+ :seq)) :s*)
           22  +	:main :seq
           23  +}))
           24  +
           25  +(defmacro putfn [tbl id & dfn]
           26  +	~(put ,tbl ,(keyword id) (fn ,(symbol tbl ":" id) ,;dfn)))
           27  +
           28  +(def modes @{})
           29  +
           30  +(def *trace?* (gensym))
           31  +(setdyn *trace?* true)
           32  +
           33  +(def failure (gensym))
           34  +(defn fail! [code & arg]
           35  +	(as-> (string/format ;arg) msg
           36  +		  (if-not (dyn *trace?*) msg
           37  +			  (debug/stacktrace (fiber/root) msg ""))
           38  +		  (return failure [code msg])))
           39  +
           40  +(defn guard* [thunk fail]
           41  +	(def [code result] (prompt failure
           42  +			(def r (thunk))
           43  +			[nil r]))
           44  +	(if code (fail code result)
           45  +		result))
           46  +
           47  +(defmacro guard [body & conds]
           48  +	(def $code (gensym))
           49  +	~(,guard* (fn guard-thunk [] ,body)
           50  +				  (fn guard-catch [,$code msg]
           51  +					  (match ,$code
           52  +						  ;conds
           53  +						  # fail upward if not caught
           54  +						  _ (return failure [code msg])))))
           55  +
           56  +(defn score-marshal [score]
           57  +	(marshal [:application/x.vt.ostinata 0
           58  +			  {:score score}]))
           59  +
           60  +(defn score-unmarshal [bin]
           61  +	(match (unmarshal bin)
           62  +		[:application/x.vt.ostinata vers {:score score}]
           63  +			score
           64  +		_ (fail! :parse "not a valid score file")))
           65  +
           66  +(defmacro enum [& vals]
           67  +	(var idx 0)
           68  +	(tabseq [:after (++ idx)
           69  +			 v :in vals]
           70  +			v idx))
           71  +
           72  +(def default-note-keys
           73  +	# second col is half-tones
           74  +	["a" "z"
           75  +	 "s" "x"
           76  +	 "d" "c"
           77  +	 "f" "v"
           78  +	 "g" "b"
           79  +	 "h" "n"
           80  +	 "j" "m"
           81  +	 "k" ","
           82  +	 "l"])
           83  +
           84  +(def note-map {
           85  +	:continuum (fn note-map:continuum [me f]
           86  +	   (seq [i :range [0 (me :n)]
           87  +				:let [ncode ((get-in me [:keys i]) 0)
           88  +					  ncap  (string/format "%c" ncode)
           89  +					  prog  (/ i (me :n))]]
           90  +			   (f prog ncap ncode)))
           91  +})
           92  +
           93  +(defn note-map-for [env chan]
           94  +	(def km (or (chan :keymap)
           95  +				(env :keymap)
           96  +				default-note-keys))
           97  +	(def nkeys (length km))
           98  +	(def note-key-indices
           99  +		(tabseq [i :range [0 nkeys]] ((km i) 0) i))
          100  +	(struct/with-proto note-map
          101  +		:keys km
          102  +		:n nkeys
          103  +		:key->index note-key-indices))
          104  +
          105  +(def <instr>
          106  +	{:start (fn <instr>start [me env note time]
          107  +				[:i (:ident-note me note) time -1
          108  +					   ;(:note me env note)])
          109  +	 :stop (fn <instr>stop [me env note time]
          110  +			   [:i (string (:ident-note me note)) time
          111  +				(get-in me [:cfg :dwell] .2)])
          112  +	 :cfg {}
          113  +	 :for-chan (fn <instr>for-chan [me env chan]
          114  +				   (def cfg (merge (me :cfg) (chan :cfg)))
          115  +				   (def intermediate
          116  +					   (struct/with-proto me
          117  +						  :cfg cfg
          118  +						  :ampl (chan :ampl)))
          119  +				   (struct/with-proto me
          120  +					  ;(kvs intermediate)
          121  +					  ;(kvs (:setup intermediate env chan))))
          122  +	 :setup (fn <instr>setup [me env chan]
          123  +				{:note-map (note-map-for env chan)})
          124  +	 :ident-note (fn <instr>ident-note [me n]
          125  +		 (def frac (reverse (string (n :id)))) # not a joke
          126  +		 (string (me :name) "." frac))})
          127  +
          128  +(defn lerp [t a b]
          129  +	(+ (* (- 1 t) a)
          130  +	   (*      t  b)))
          131  +
          132  +(defn freq-spread-notes [me env chan]
          133  +	(def super ((<instr> :setup) me env chan))
          134  +	(defn cv [x &opt dflt] (get-in me [:cfg x] dflt))
          135  +	(merge super
          136  +		   {:notes (:continuum (super :note-map)
          137  +					   (fn [p cap code]
          138  +						   (lerp p  (cv :base)
          139  +								 (+ (cv :base) (cv :span)))))}))
          140  +(def orchestra {
          141  +	:sine (struct/with-proto <instr>
          142  +				:name "sine"
          143  +				:cfg {:base 800 :span 600 :chime 1}
          144  +				:setup freq-spread-notes
          145  +				:note (fn sine:note [me env n]
          146  +					(def freq (get-in me [:notes (n :index)]))
          147  +					[(* (me :ampl) (n :vel)) freq (get-in me [:cfg :chime])]))
          148  +	:string (struct/with-proto <instr>
          149  +				:name "string"
          150  +				:cfg {:base 500 # shortest frequency
          151  +					  :span 300 # range of frequencies
          152  +					  :decay 0}
          153  +				:setup freq-spread-notes
          154  +				:note (fn drum:note [me env n]
          155  +					(def freq (get-in me [:notes (n :index)]))
          156  +					(def c (me :cfg))
          157  +					[(* (me :ampl) (n :vel)) freq (- 1 (* .1 (c :decay)))]))
          158  +})
          159  +
          160  +
          161  +(defn timecode→string [t]
          162  +	(def μs             (%    t        1))
          163  +	(def  s (math/floor (%    t        60)))
          164  +	(def  m (math/floor (% (/ t 60)    60)))
          165  +	(def  h (math/floor (% (/ t 60) (* 60 60))))
          166  +	(string/format "%02u:%02u:%02u.%02u" h m s
          167  +				   (math/floor (* μs 100))))
          168  +
          169  +(defn cmd→string [cmd]
          170  +	(defn txl [val]
          171  +		(cond (keyword? val) (string val)
          172  +			:else (string/format "%q" val)))
          173  +	(def str (string/join (map txl cmd) " ")))
          174  +
          175  +(defn sort-score [env]
          176  +	(sort-by |($ :at) (env :score)))
          177  +
          178  +(defn over-score ``
          179  +	iterates a score's notes, respecting mute flags
          180  +	and the the current isolation mode
          181  +	`` [env f]
          182  +	(each n (env :score)
          183  +		(def chan-id (n :chan))
          184  +		(def chan (get-in env [:chans chan-id]))
          185  +		(when (and (not (chan :mute))
          186  +				   (or (empty?     (env :iso))
          187  +					   (has-value? (env :iso) chan-id)))
          188  +			(f n))))
          189  +
          190  +(defn write-score [env]
          191  +	(def cmds @[])
          192  +	(sort-score env)
          193  +	(over-score env (fn [n]
          194  +		(def chan (get-in env [:chans (n :chan)]))
          195  +		(def ins (:instr-for-chan env chan))
          196  +		(defn ins-cmd [method & args]
          197  +			((ins method) ins env ;args))
          198  +		(def cl [(ins-cmd :start n (n :at))
          199  +				 (ins-cmd :stop n (+ (n :at) (n :len)))])
          200  +		(each c cl (array/push cmds c))))
          201  +	(def score-text
          202  +		(string/join (map |(string (cmd→string $) "\n") cmds)))
          203  +	score-text)
          204  +
          205  +(defn player [env]
          206  +	(sort-score env)
          207  +	(def chan-instrs
          208  +		(tabseq [[chid chan] :pairs (env :chans)]
          209  +				chid (:instr-for-chan env chan)))
          210  +	(var last-time (env :time))
          211  +	(protect (over-score env (fn [e]
          212  +		(unless (< (e :at) last-time) #TODO cache last note id in play mode?
          213  +			# (def chan (get-in env [:chans (e :chan)]))
          214  +			(def ins (in chan-instrs (e :chan)))
          215  +			(defn ins-cmd [method & args]
          216  +				((ins method) ins env ;args))
          217  +
          218  +			# skip over notes we've already passed
          219  +			(def t (e :at))
          220  +			(ev/sleep (- t last-time))
          221  +			(set last-time t)
          222  +
          223  +			#haack
          224  +			(:send env ;(ins-cmd :start e 0))
          225  +			(:send env ;(ins-cmd :stop e (e :len))))))))
          226  +
          227  +(putfn modes play [env]
          228  +	(defn ticks []
          229  +		(os/clock :monotonic))
          230  +	(var rec false)
          231  +	(var cur-id (if (empty? (env :score)) 0
          232  +					(+ 1 (extreme > (map |($ :id) (env :score))))))
          233  +	(def start-time (env :time))
          234  +
          235  +	(var held @{})
          236  +	(var last-time (ticks))
          237  +	(def chan (get-in env [:chans (env :chan)]))
          238  +	(var loud 1)
          239  +	(def ins (:instr-for-chan env chan))
          240  +	(defn note-key? [k]
          241  +		(not (nil? (get-in ins [:note-map :key->index k]))))
          242  +	(defn draw-stat-line [timecode]
          243  +		(def sustain @[])
          244  +		(defn clr [& r] (:color (env :tty) ;r))
          245  +		# (defn vbar [& r] (:vbar (env :tty) ;r))
          246  +		(each n (get-in ins [:note-map :keys])
          247  +			(when-let [k (get held n nil)]
          248  +				(def k (string/format "%c" n))
          249  +				(array/push sustain k)))
          250  +		(:msg env "\r\e[2K%s\r"
          251  +			  (string
          252  +				  (if rec "\e[91m⏺"
          253  +					      "\e[94m⏹") "\e[m"
          254  +				  (clr [.9 .95 1] [0.1 0.2 0.4]
          255  +					  (string " " (timecode→string timecode) " "))
          256  +				  (:vol-bar env (/ loud 11))
          257  +				  # (vbar (/ loud 11)
          258  +					# 	[.5 1 .25]
          259  +					# 	[1.0 1.0 0.25]
          260  +					# 	[.8 0 0])
          261  +				  " " (chan :instr) "\e[96m@" (env :chan) "\e[m"
          262  +				  " \e[1m" (if rec "record" "play") "\e[m"
          263  +				  (string/join sustain))))
          264  +
          265  +	(def timewarp (get-in chan [:cfg :timewarp] 1.0))
          266  +
          267  +	(defn advance-clock [res]
          268  +		(protect (forever
          269  +			(def cur-time (ticks))
          270  +			(draw-stat-line (+ (env :time)
          271  +							   (* (- cur-time last-time)
          272  +								  timewarp)))
          273  +			(ev/sleep res))))
          274  +
          275  +	(var clock-fiber nil)
          276  +	(var player-fiber nil)
          277  +	(defn start-clock []
          278  +		(set clock-fiber  (fiber/new |(advance-clock 0.01)))
          279  +		(set player-fiber (fiber/new |(player env)))
          280  +		(ev/go clock-fiber)
          281  +		(ev/go player-fiber))
          282  +	(defn stop-clock []
          283  +		(when clock-fiber
          284  +			(ev/cancel clock-fiber nil)
          285  +			(set clock-fiber nil))
          286  +		(when player-fiber
          287  +			(ev/cancel player-fiber nil)
          288  +			(set player-fiber nil)))
          289  +
          290  +	(defn ins-cmd [method & args]
          291  +		((ins method) ins env ;args))
          292  +	(defn stop-all-notes []
          293  +		(eachp [k v] held
          294  +			(:send env ;(ins-cmd :stop v 0))))
          295  +	(defn cleanup []
          296  +		(stop-all-notes)
          297  +		(stop-clock))
          298  +	(def score-staging @[])
          299  +
          300  +
          301  +	(defer (cleanup) (prompt teardown (forever
          302  +		(draw-stat-line (env :time))
          303  +		(def event (yield))
          304  +
          305  +		# get precise time unaffected by clock refresh rate
          306  +		(def cur-time (ticks))
          307  +		(def advance (- cur-time last-time))
          308  +		(set last-time cur-time)
          309  +
          310  +		(when rec (+= (env :time) (* advance timewarp)))
          311  +		(def next-id cur-id)
          312  +		(++ cur-id)
          313  +
          314  +		(defn store-note [b]
          315  +			(def stored (alg/qrec b
          316  +			  :at :mods :vel :id :index :chan))
          317  +			
          318  +			(def len (- cur-time (b :t)))
          319  +			(array/push score-staging
          320  +						(merge stored {:chan (env :chan)
          321  +									   :len len})))
          322  +
          323  +		(defn commit-score []
          324  +			(unless (empty? score-staging)
          325  +				(array/concat (env :score) score-staging)
          326  +				(array/clear score-staging)
          327  +				(sort-score env)))
          328  +
          329  +		(defn commit-unless-shift []
          330  +			(unless (string/check-set (event :mods) :s)
          331  +				(commit-score)))
          332  +		
          333  +		(match event
          334  +			{:how :press :key (@ (comptime ("q" 0)))}
          335  +				(do (stop-all-notes)
          336  +					(:set-mode env :cmd)
          337  +					(commit-unless-shift))
          338  +			{:how :press :key (@ (comptime ("r" 0)))}
          339  +				(do (stop-all-notes)
          340  +					(toggle rec)
          341  +					(if rec (start-clock) (stop-clock))
          342  +					(commit-unless-shift))
          343  +			{:how :press   :key :shift-l} (+= loud 1)
          344  +			{:how :release :key :shift-l} (-= loud 1)
          345  +			{:how :press   :key :shift-r} (-= loud .5)
          346  +			{:how :release :key :shift-r} (+= loud .5)
          347  +			{:how :press   :key :tab}
          348  +				(do (put env :time start-time)
          349  +					(stop-clock) (start-clock))
          350  +			({:how :press :key n}
          351  +				 (<= 0x31 n 0x39))
          352  +				(set loud (- n 0x30))
          353  +			({:how :press :key k}
          354  +				 (note-key? k)
          355  +				 (nil? (held k)))
          356  +				(do (def note {:id cur-id  :vel loud
          357  +							   :t cur-time :at (env :time)
          358  +							   :index (get-in ins [:note-map :key->index k])
          359  +							   :mods (get event :mods :)})
          360  +					(put held k note)
          361  +					(:send env ;(ins-cmd :start note 0)))
          362  +			({:how :release :key k} (note-key? k))
          363  +				(when (has-key? held k)
          364  +					(def b (held k))
          365  +					(:send env ;(ins-cmd :stop b 0))
          366  +
          367  +					(when rec (store-note b))
          368  +					
          369  +					(put held k nil)
          370  +					)
          371  +
          372  +			)))))
          373  +
          374  +(putfn modes ev [env]
          375  +	   (prompt teardown (forever
          376  +			(def event (yield))
          377  +			(match event
          378  +				{:how :press :text "q"}
          379  +					(:set-mode env :cmd) 
          380  +				_ (:msg env "event packet: %P\r\n"
          381  +						(struct/proto-flatten event))
          382  +
          383  +				))))
          384  +
          385  +(defn mk-channel []
          386  +	@{:instr :sine
          387  +	  :ampl 1
          388  +	  :cfg @{}
          389  +	  :mute false})
          390  +
          391  +(defn present-file [env text]
          392  +	(def lines (string/split "\n" text))
          393  +	(defn c [& r] (:color (env :tty) ;r))
          394  +	(var idx 0)
          395  +	(def out-lines @[])
          396  +	(each line lines (++ idx)
          397  +		(array/push out-lines
          398  +			(string (c [.6 .2 .3] nil
          399  +					   (string/format "% 04d" idx))
          400  +					" " line "\n")))
          401  +	(string/join out-lines))
          402  +
          403  +(defn do-cmd [str env]
          404  +	(def argv (peg/match grammar str))
          405  +	(defn say [fmt & args]
          406  +		(:write (env :tty) (string/format fmt ;args)))
          407  +	(def chan (get-in env [:chans (env :chan)]))
          408  +	(defn bad-cmd []
          409  +		(:msg env "bad command\n"))
          410  +	(match argv
          411  +		@["q"] (return terminate)
          412  +		@["t" time] (put env :time (scan-number time))
          413  +		@["f" path] (put env :file path)
          414  +		@["t"] (:msg env "time is %f\n" (env :time))
          415  +		@["f"] (if (= "" (env :file))
          416  +				   (:msg env "no file\n")
          417  +				   (:msg env "file is %s\n" (env :file)))
          418  +
          419  +		@["ch" "ls"] (do (defn c [& r] (:color (env :tty) ;r))
          420  +						 (def tpl (string
          421  +									  (c [.8 .3 .4] nil
          422  +										 "% 3i ") #idx
          423  +
          424  +									  "%s%s " #iso-state volume
          425  +									  (c [.5 .5 1] [0 0 .1] " %s ") #name
          426  +									  (c [1 .5 .5] [.1 0 0] " %s ") #instr
          427  +									  "\n"))
          428  +						 (def max-ampl (extreme >
          429  +							(map (fn [(_ ch)] (ch :ampl))
          430  +								 (pairs (env :chans)))))
          431  +						 (defn iso-state [id ch]
          432  +							 (def colors {:iso   [.2 1 .1]
          433  +										  :uniso [1 .2 .1]
          434  +										  :mute  [.1 .4 1]
          435  +										  :on    [1 1 1]
          436  +										  :blank [0 0 0]})
          437  +							 (def states @[])
          438  +							 (when (ch :mute) (array/push states :mute))
          439  +							 (unless (empty? (env :iso))
          440  +								 (array/push states
          441  +									 (if (has-value? (env :iso) id)
          442  +										 :iso :uniso)))
          443  +							 (def leds (string
          444  +								(if (ch :mute)                 "m" " ")
          445  +								(if (has-value? states :iso)   "i" " ")))
          446  +							 # (unless (or (ch :mute)
          447  +								# 		 (has-value? states :uniso))
          448  +								#  (array/push states :on))
          449  +							 (when (empty? states)
          450  +								 (array/push states :blank))
          451  +							 
          452  +							 (def base (let [n (/ 1 (length states))]
          453  +								 (alg/vect * [n n n]
          454  +								   (alg/vect + ;(map |(colors $) states)))))
          455  +							 (defn vlerp [t a b]
          456  +								 (alg/vect |(lerp t $ $1) a b))
          457  +							 (c (vlerp 0.5 base [1 1 1])
          458  +								(vlerp 0.5 base [0 0 0])
          459  +								leds))
          460  +
          461  +						 (var idx 0)
          462  +						 (each [id ch] (pairs (env :chans))
          463  +							 (++ idx)
          464  +							 (:msg env tpl
          465  +								   idx
          466  +								   (iso-state id ch)
          467  +								   (:vol-bar env (/ (ch :ampl) max-ampl))
          468  +								   id (ch :instr))
          469  +							 (unless (empty? (ch :cfg))
          470  +								 (:msg env "         %s\n" (string/join
          471  +								 (map |(string/format
          472  +										   (string (c [.2 .4 1] nil "\e[1m%s")
          473  +												   "="
          474  +												   (c [.6 .3 1] nil "%q"))
          475  +										   ;$)
          476  +									  (pairs (ch :cfg))) " ")))))
          477  +		(@["ch" "mv" f op t]
          478  +				(has-value? ["to" "clobber" "merge"] op))
          479  +			(do (def (from to) (map keyword [f t]))
          480  +				(cond
          481  +					(and (not= op "clobber") (has-key? (env :chans) to))
          482  +						(:msg env "cannot clobber existing channel")
          483  +					(and (= op "merge") (has-key? (env :chans) to))
          484  +						(put-in env [:chans from] nil)
          485  +					:else # valid move
          486  +					(do (put-in env [:chans to  ] (get-in env [:chans from]))
          487  +						(put-in env [:chans from] nil)))
          488  +
          489  +				(when (= (env :chan) from)
          490  +					(put env :chan to))
          491  +				(each s (env :score)
          492  +					(when (= from (s :chan))
          493  +						(put s :chan to))))
          494  +
          495  +		@["ch" "set" c] (let [kc (keyword c)]
          496  +					  (put env :chan kc)
          497  +					  (unless (has-key? (env :chans) kc)
          498  +						  (put-in env [:chans kc] (mk-channel))))
          499  +		@["ch" _] (bad-cmd)
          500  +		@["ch"] (:msg env "current channel is %s\n" (env :chan))
          501  +
          502  +		@["perf"] (:msg env "performing score\n"
          503  +						(ev/write (get-in env [:cs-in 1])
          504  +								  (write-score env)))
          505  +		@["iso" "set" & chans] (put env :iso (map keyword chans))
          506  +		@["iso" "add" & chans] (array/concat (env :iso) (map keyword chans))
          507  +		@["iso" "del" & chans]
          508  +			(put env :iso (filter |(not (has-value? chans (string $)))
          509  +								  (env :iso)))
          510  +		@["iso" "off"] (array/clear (env :iso))
          511  +		@["iso" _] (bad-cmd)
          512  +		@["iso"] (:msg env "isolating channels %s\n"
          513  +					   (string/join (map string (env :iso)) " "))
          514  +
          515  +		@["amp" x] (put chan :ampl (scan-number x))
          516  +		@["amp"] (:msg env "channel %s amp is %f\n" (env :chan) (chan :ampl))
          517  +
          518  +		@["mute" "on"] (put chan :mute true)
          519  +		@["mute" "off"] (put chan :mute false)
          520  +		@["mute"] (do (toggle (chan :mute))
          521  +					  (:msg env "%s channel %s\n"
          522  +							(if (chan :mute) "muting" "unmuting")
          523  +							(env :chan)))
          524  +
          525  +		@["cfg" key val] (put-in chan [:cfg (keyword key)]
          526  +								 (scan-number val))
          527  +		@["cfg" key] (:msg env "%s = %s\n" key
          528  +						   (string (get-in chan [:cfg (keyword key)] 
          529  +										   "\e[3m<unset>\e[m")))
          530  +		@["cfg"] (each [k v] (pairs (chan :cfg))
          531  +					 (:msg env "\e[1m%s\e[m = %s\n" k (string v)))
          532  +		@["sc" "from" t] (:msg env "printing score from %f\n" (env :time))
          533  +		@["sc" "to" t] (:msg env "printing score to %f\n" (env :time))
          534  +		@["sc" "erase" "all"] (array/clear (env :score))
          535  +		@["sc" "erase"] nil
          536  +		@["sc" "comp" path] (do (:msg env "writing score to %s\n" path)
          537  +								(spit path (write-score env)))
          538  +		@["sc" "comp"] (:msg env "composing score\n%s\n"
          539  +							 (present-file env (write-score env)))
          540  +		@["sc"] (:msg env "printing score\n%P\n" (env :score))
          541  +
          542  +		@["pl"] (:set-mode env :play) 
          543  +		@["in" inst] (let [ik (keyword inst)]
          544  +						 (if (has-key? orchestra ik) (put chan :instr ik)
          545  +							 (:msg env "no such instrument %s\n" inst)))
          546  +		@["in"] (:msg env "instrument is %s\n" (chan :instr))
          547  +		@["st" "pid"] (:msg env "csound process is pid %i\n"
          548  +							(get-in env [:cs-proc :pid]))
          549  +		@["st" "ev"] (:set-mode env :ev)
          550  +		(x (or (nil? x) (empty? x))) nil
          551  +		(:msg env "bad cmd\n")))
          552  +
          553  +(putfn modes cmd [env]
          554  +	(var cmd-line @"")
          555  +	(def history @[])
          556  +	(var histidx nil)
          557  +	(prompt teardown (forever
          558  +		(:msg env "\r\e[2K\e[1m:%s\e[m" cmd-line)
          559  +		(def event (yield))
          560  +		(defn go-history [n]
          561  +			(def lastidx (- (length history) 1))
          562  +			(label give-up
          563  +					(cond (empty? history) (return give-up)
          564  +						(and (nil? histidx)
          565  +							   (< n 0)) (set histidx (+ n (length history)))
          566  +						(nil? histidx) (return give-up)
          567  +						(> (+ n histidx) lastidx)
          568  +							 (do (set histidx nil)
          569  +								 (set cmd-line @"")
          570  +								 (return give-up))
          571  +						(<= (+ n histidx) lastidx)
          572  +							(set histidx (max 0 (+ n histidx)))
          573  +						:else (return give-up))
          574  +					(set cmd-line (buffer (history histidx)))))
          575  +		(when (or (= (event :how) :press)
          576  +				  (= (event :how) :repeat))
          577  +			(match event
          578  +				{:key :bksp} (buffer/popn cmd-line 1)
          579  +				{:key :tab} (do)
          580  +				{:key :up} (go-history -1)
          581  +				{:key :dn} (go-history  1)
          582  +				{:key :enter} (do
          583  +					(:msg env "\r\n")
          584  +					(set histidx nil)
          585  +					(do-cmd cmd-line env)
          586  +					(array/push history (string cmd-line))
          587  +					(buffer/clear cmd-line))
          588  +				{:text t} (buffer/push cmd-line t)
          589  +				)))))
          590  +
          591  +(defn task-cmd [env]
          592  +	"command input handling"
          593  +	(def {:tty tty} env)
          594  +	(var mode nil)
          595  +
          596  +	(defn new-mode [x & args]
          597  +		(def m (fiber/new x :yi))
          598  +		(resume m env ;args)
          599  +		(fn resume-mode [& args] (resume m ;args)))
          600  +
          601  +	(put env :set-mode (fn env:set-mode [_ x & args]
          602  +						   (set mode (new-mode (modes x) ;args))
          603  +						   (return teardown)
          604  +						   ))
          605  +
          606  +	# this is silly
          607  +	(prompt teardown (:set-mode env :cmd))
          608  +
          609  +	(prompt terminate
          610  +		(loop [e :iterate (:await tty)
          611  +				 :while mode]
          612  +			(mode e)))
          613  +
          614  +	(os/proc-kill (env :cs-proc) true :term))
          615  +
          616  +(defn main [& argv]
          617  +	(def cs-in  (os/pipe :R))
          618  +	(def cs-out (os/pipe :W))
          619  +	(def cs (os/spawn ["csound" "-Lstdin" "--orc" "ostinata.orc" "-odac"] :p
          620  +					  {:in (cs-in 0) :out (cs-out 1) :err (cs-out 1)}))
          621  +	(def tty (tui/init (os/open "/dev/tty" :rw)))
          622  +	(def doc @{})
          623  +	(def env @{:tty tty
          624  +			   :score @[]
          625  +			   :chans @{:root (mk-channel)}
          626  +			   :iso []
          627  +			   :chan :root
          628  +			   :file ""
          629  +			   :pause false
          630  +			   :time 0.0
          631  +			   :cs-proc cs
          632  +			   :cs-in cs-in
          633  +			   :cs-out cs-out
          634  +			   :vol-bar (fn [me fac] (:vbar (me :tty) fac
          635  +											[.5 1 .25]
          636  +											[1.0 1.0 0.25]
          637  +											[1 .2 .1]))
          638  +			   :instr-for-chan (fn [me c]
          639  +								   (:for-chan (orchestra (c :instr)) me c))
          640  +			   :send (fn env:send [me & args]
          641  +						 (def cmd (cmd→string args))
          642  +						 # (print "cmd = " cmd)
          643  +						 (ev/write (cs-in 1) (string cmd "\n")))
          644  +			   :msg (fn env:msg [me fmt & args]
          645  +					   (:write (me :tty) (string/format fmt ;args)))
          646  +			  })
          647  +		(ev/call |(do (defer (:close tty)
          648  +						  (task-cmd env)))))

Added ostinata/ostinata.orc version [fcb089814a].

            1  +0dbfs = 100
            2  +nchnls = 2
            3  +sr = 44100
            4  +
            5  +; p2 when
            6  +; p3 dur
            7  +; p4 vol
            8  +; p5 freq
            9  +
           10  +givol ftgen 1, 0, 10000, 25, \
           11  +	500, 60, \
           12  +	1000, 20, \
           13  +	7000, 3, \
           14  +	10000, 5
           15  +
           16  +instr sine
           17  +	tigoto skipinit
           18  +	kt init 0
           19  +	al init 1.0
           20  +	ifreq = p5
           21  +	ichime = p6
           22  +
           23  +	ivol = 30 * p4 * table(ifreq, givol)
           24  +
           25  +	aw oscils ivol, ifreq, 0
           26  +	af oscils ivol, ifreq * ichime, 0
           27  +	skipinit:
           28  +	kt = kt + (1/kr)
           29  +	kv bpf kt, 0,0, 0.5,0.8, .1,1, 1,.6
           30  +	at linseg 0, p3, 1
           31  +
           32  +	if p3 >= 0 then
           33  +		aw = aw * (1 - at)
           34  +		aw = aw + (af * at)
           35  +		aw = aw * (1-at)
           36  +	endif
           37  +
           38  +	out aw*al
           39  +endin
           40  +
           41  +instr string
           42  +	tigoto skipinit
           43  +	; initialize
           44  +	ivol = 30*p4
           45  +	ifreq = p5
           46  +
           47  +	aw pluck ivol, ifreq, ifreq, 0, 3, p6
           48  +	skipinit:
           49  +	at linseg 1, p3, 0
           50  +
           51  +	if p3 >= 0 then
           52  +		aw = aw * at
           53  +	endif
           54  +	out aw
           55  +endin
           56  +

Added ostinata/tui.janet version [02341fd427].

            1  +# [ʞ] tui.janet
            2  +#  ~ lexi hale <lexi@hale.su>
            3  +#  🄯 AGPLv3
            4  +#  ? quick and extremely dirty rawterm interface
            5  +
            6  +(use /alg)
            7  +
            8  +# FIXME use ffi to directly munge termios where feasible
            9  +(defn raw [fd] 
           10  +	(os/execute ["stty" "raw" "-echo" "opost"] :p))
           11  +
           12  +(defn unraw [fd]
           13  +	(os/execute ["stty" "-raw" "echo"] :p))
           14  +
           15  +(defmacro use-raw [fd]
           16  +	~(upscope
           17  +		 (,raw ,fd)
           18  +		 (,defer ,unraw ,fd)))
           19  +
           20  +(defmacro with-raw [fd & x]
           21  +	~(do (,raw ,fd)
           22  +		 ,;x
           23  +		 (,unraw ,fd)))
           24  +
           25  +(def- capq-resp (peg/compile ~{
           26  +	:lst (* '(+ (some (range "09"))
           27  +				(constant "0"))
           28  +			  (? (* ";" :lst)))
           29  +	:resp "\e[?"
           30  +	:seq (* :resp (group :lst))
           31  +	:tc (* :seq "c")
           32  +	:pg (* :resp (number (any (range "09"))) "u")
           33  +	:main (* (+ :pg (constant false))
           34  +			  :tc)
           35  +}))
           36  +(def- pg-flags {0x01 :E # disambig events
           37  +			    0x02 :e # report evt types
           38  +			    0x04 :k # keycode alts
           39  +			    0x08 :K # all keys as alts
           40  +			    0x16 :t}) # text
           41  +(defn release [env]
           42  +	(defn send [& x] (ev/write (env :fd) (string ;x)))
           43  +	(when (string/check-set (env :caps) :p)
           44  +		(send "\e[<1u"))
           45  +	(unraw (env :fd))
           46  +	(:close (env :fd)))
           47  +
           48  +(defn- read-until [pred fd]
           49  +	(def buf @"")
           50  +	(label matched
           51  +		   (forever
           52  +			   # (ev/write fd (string/format "buf: %j\r\n" buf))
           53  +			   (ev/read fd 1 buf)
           54  +			   (spit "dbgkeys" buf)
           55  +			   (def p (pred buf))
           56  +			   (when p (return matched p)))))
           57  +
           58  +(defn- text-key [c & args]
           59  +	(struct :text c
           60  +			:key ((string/ascii-lower c) 0)
           61  +			:case (c 0)
           62  +			:base (c 0)
           63  +			:how :press
           64  +			;args))
           65  +
           66  +(def- event-code-ptn (peg/compile ~{
           67  +	:csi "\e["
           68  +	:ss3 "\e\x4f"
           69  +	:cs3 (+ :csi :ss3)
           70  +	:key-basic (cmt (* :csi :keycode :keyev "~" )
           71  +				,(fn [kc ks kb mods how]
           72  +					(def basis
           73  +						(case kc
           74  +						   2 :ins
           75  +						   3 :del
           76  +						   5 :pgup
           77  +						   6 :pgdn
           78  +						   7 :home
           79  +						   8 :end
           80  +						   (cond (<= 11 kc 24) (keyword :f (- kc 11))
           81  +							     :else :unknown)))
           82  +					 {:key  basis
           83  +					  :mods mods
           84  +					  :how how}))
           85  +	:key-magic (cmt (* :csi "1" :keyev '(+ (range "AF") "H" "P" "Q" "S"))
           86  +					,(fn [mods how cue]
           87  +						{:mods mods
           88  +						 :how how
           89  +						 :key (case cue
           90  +								  "A" :up "C" :rt
           91  +								  "B" :dn "D" :lf
           92  +								  "H" :home "F" :end
           93  +								  "E" :kp-begin )}))
           94  +	:legacy-ctl (cmt '(range "\x01\x07" "\x0b\x0c" "\x0e\x1a")
           95  +			        ,(fn [ccode]
           96  +						(def code (+ 0x60 (ccode 0)))
           97  +						(def lc (string/format "%c" code))
           98  +						{ :text lc
           99  +						  :key code
          100  +						  :case code
          101  +						  :base code
          102  +						  :how :press
          103  +						  :mods :c }))
          104  +	:key (+ (* "\x0D"            (constant {:key :enter :how :press}))
          105  +	        (* (+ "\x7F" "\x08") (constant {:key :bksp  :how :press}))
          106  +			(* "\x09"            (constant ,(text-key "\t" :key :tab)))
          107  +			(* "\x00"            (constant ,(text-key " "  :key :sp :mods :c)))
          108  +			:legacy-ctl
          109  +	        (* :cs3 "A"          (constant {:key :up :how :press}))
          110  +	        (* :cs3 "B"          (constant {:key :dn :how :press}))
          111  +	        (* :cs3 "C"          (constant {:key :rt :how :press}))
          112  +	        (* :cs3 "D"          (constant {:key :lf :how :press}))
          113  +	        (* :cs3 "E"          (constant {:key :kp-begin :how :press}))
          114  +			(+ (* :csi :keyspec "u")
          115  +			   (* "\e"  (cmt :legacy-ctl
          116  +						,(fn [c] (struct/with-proto c
          117  +									:mods (keyword (get c :mods :) :a)))))
          118  +			   (* "\e" (not "[") (cmt '1 ,|(text-key $ :mods :a)))
          119  +			   (*     (not "\e") (cmt '1 ,|(text-key $)))))
          120  +	:ctl (cmt :num0 ,(fn [c]
          121  +			(defn b [n k]
          122  +				(if (not= 0 (band (- c 1) n)) k :))
          123  +			(keyword (b 0x01 :s) (b 0x10 :H)
          124  +			         (b 0x02 :a) (b 0x20 :m)
          125  +			         (b 0x04 :c) (b 0x40 :C)
          126  +			         (b 0x08 :S) (b 0x80 :N))))
          127  +	:keyev (+ (* ";" :ctl
          128  +	             (+ (* ":1" (constant :press))
          129  +	                (* ":2" (constant :repeat))
          130  +	                (* ":3" (constant :release))
          131  +	                (constant :press)))
          132  +			   (* ";" (constant :) (constant :press))
          133  +	           (* (constant :) (constant :press)))
          134  +	:text (+ (cmt (group (some (* ";" :num0)))
          135  +				 ,(fn [chars] #FIXME
          136  +					(string ;(map |(string/format "%c" $) chars))))
          137  +			 (constant false))
          138  +	:keycode (+ (* :num0 ":" :num0 ":" :num0)
          139  +	            (* :num0 ":" :num0 (constant false))
          140  +	            (* :num0 (constant false) (constant false)))
          141  +	:keyspec (cmt (* :keycode :keyev :text)
          142  +				,(fn keyspec-translate [kc ks kb mods how t]
          143  +					(def basis
          144  +						(case kc
          145  +						   9 {:key :tab }
          146  +						   13 {:key :enter }
          147  +						   127 {:key :bksp }
          148  +
          149  +						   57358 {:key :lock-caps }
          150  +						   57360 {:key :lock-num }
          151  +						   57414 {:key :kp-enter }
          152  +						   57413 {:key :kp-plus  :text "+"}
          153  +						   57412 {:key :kp-minus :text "-"}
          154  +						   57411 {:key :kp-mul   :text "*"}
          155  +						   57410 {:key :kp-div   :text "/"}
          156  +						   43    {:key :add :text "+"}
          157  +						   8722  {:key :sub :text "−"}
          158  +						   215   {:key :mul :text "×" }
          159  +						   243   {:key :div :text "÷" }
          160  +						   57441 {:key :shift-l} 57447 {:key :shift-r}
          161  +						   57443 {:key :alt-l}   57449 {:key :alt-r}
          162  +						   57444 {:key :super-l} 57450 {:key :super-r}
          163  +						   57445 {:key :hyper-l} 57451 {:key :hyper-r}
          164  +						   57442 {:key :ctl-l}   57448 {:key :ctl-r}
          165  +						   57453 {:key :lvl3}
          166  +						   57454 {:key :lvl5}
          167  +						   (cond (<= 57376 kc 57398) {:key (keyword :f (- kc 57363))}
          168  +								:else {:key kc
          169  +									   :text (or t (string/format "%c" (or ks kc)))})))
          170  +					 {:text (get basis :text nil)
          171  +					  :key  (get basis :key nil)
          172  +					  :case (or ks kc)
          173  +					  :base (or kb kc)
          174  +					  :mods mods
          175  +					  :how how}))
          176  +	:main (* (+ :key-basic :key-magic :key) -1)
          177  +
          178  +	:num (some (range "09"))
          179  +	:num0 (+ (number :num)
          180  +	         (constant 0))
          181  +}))
          182  +
          183  +(def- color-cmd
          184  +	(do (def env (os/environ))
          185  +		(def ct (env "COLORTERM"))
          186  +		(def term (env "TERM"))
          187  +		(defn mode-char [mode]
          188  +			(case mode :fg    "3" :bg    "4"
          189  +				       :fg-br "9" :bg-br "10"
          190  +				(error "bad mode")))
          191  +
          192  +		(cond (or (= ct "truecolor")
          193  +				  (= term "xterm")) # modern xterm supports tc
          194  +				(fn colorize-tc [mode rgb]
          195  +					(as-> rgb v
          196  +						(map |(math/floor (* 255 (clamp $))) v)
          197  +						(string/join (map string v) ";")
          198  +						(string "\e[" (mode-char mode) "8;2;" v "m")))
          199  +
          200  +			(or (string/has-suffix? "256color" term)
          201  +				(not (nil? ct)))
          202  +				(fn colorize-256 [mode rgb]
          203  +					(def code (math/floor
          204  +						(cond (= 0 ;rgb) 0
          205  +						      (=   ;rgb) (lerp (rgb 0) 232 255)
          206  +						      :else      (+ 16 ;(vect * [36 6 1]
          207  +													  (map clamp rgb))))))
          208  +					(string "\e[" (mode-char mode) "8;5;" code "m"))
          209  +
          210  +			:else
          211  +				(fn colorize-8 [mode rgb] "")))) #FIXME
          212  +
          213  +(defn- vbar [tty f & grad]
          214  +	(def c [ " " "▁" "▂" "▃" "▄" "▅" "▆" "▇" "█" ])
          215  +	# (def fg (vect |(lerp f ;$&) bright dim))
          216  +	(def fg (vlut f grad))
          217  +	(def bg (vect |(lerp .5 ;$&) fg [0 0 0]))
          218  +	(def idx (math/floor (* (max 0 (min 1 f))
          219  +							(dec (length c)))))
          220  +	(:color tty fg bg (c idx)))
          221  +
          222  +(def- <env> {:close release
          223  +			 :write |(ev/write ($ :fd) $1)
          224  +			 :read  |(ev/read  ($ :fd) $1)
          225  +			 :color (fn tui:color [me fg bg str]
          226  +				(as-> str cmd
          227  +					(if fg (string (color-cmd :fg fg) cmd) cmd)
          228  +					(if bg (string (color-cmd :bg bg) cmd) cmd)
          229  +					(if (or fg bg) (string cmd "\e[m"))))
          230  +			 :vbar vbar
          231  +			 :await (fn tui:await [me]
          232  +			 # (print "awaiting")
          233  +				(def [evt] (read-until |(peg/match event-code-ptn $) (me :fd)))
          234  +				evt)})
          235  +
          236  +# (def- chord-pat (peg/compile ~{
          237  +# 	:wd (some (* (not "-") wd))
          238  +# }))
          239  +
          240  +(defn- subset [a b]
          241  +	(var matches true)
          242  +	(loop [[k v] :pairs a :while matches]
          243  +		(when (not= v (get b k nil))
          244  +			(set matches false)))
          245  +	matches)
          246  +(defn chord [& fm]
          247  +	(def ksym (last fm))
          248  +	(def mods (keyword ;(slice fm 0 -2)))
          249  +	(merge {:mods mods
          250  +		    :how :press}
          251  +		(cond
          252  +			(keyword? ksym) {:key ksym}
          253  +			(string?  ksym)
          254  +				{#:text ksym
          255  +				 :key (in (string/ascii-lower ksym) 0)
          256  +				 :case (in ksym 0)})))
          257  +(defn event<chord? [evt & fm]
          258  +	(subset evt (chord ;fm)))
          259  +
          260  +(defn init [fd]
          261  +	(raw fd)
          262  +	(defn send [& x] (ev/write fd (string ;x)))
          263  +	(defn recv [n &opt buf] (string (ev/read fd n buf)))
          264  +	(var caps :)
          265  +	(send "\e[?u\e[c") # query caps
          266  +	(def lists (label done
          267  +					  (def buf @"")
          268  +					  (forever
          269  +						  (recv 1 buf)
          270  +						  (def m (peg/match capq-resp buf))
          271  +						  (when m
          272  +							  (return done m)))))
          273  +	(defn set-caps! [which tbl]
          274  +		(each e which
          275  +			(when (has-key? tbl e)
          276  +				(set caps (keyword caps (tbl e))))))
          277  +	(set-caps! (lists 1) {
          278  +	   "4" :6 # sixel
          279  +	})
          280  +
          281  +	(when (lists 0)
          282  +		(set caps (keyword caps :p))
          283  +		(each [c f] (pairs pg-flags)
          284  +			(when (not= 0 (band c (lists 0)))
          285  +				(set caps (keyword caps f)))))
          286  +
          287  +	(when (string/check-set caps :p)
          288  +		(def fmap (invert pg-flags))
          289  +		(def f (bor (lists 0) (fmap :k) (fmap :e) (fmap :K)))
          290  +		(send "\e[>" (string (inc f) "u")))
          291  +
          292  +	(struct/with-proto <env>
          293  +					   :fd fd
          294  +					   :caps caps))
          295  +