Sunday, 9 June 2024

bitzliten: sound looper with Svelte 5

Following on from our last struggle to build nice features that was more about reactivity and refactoring.

Let us build:

Noisescape Visual

I look around for the tech. This has nice high-level features but is super slow:

Clementine does something that shows episodes of different sonic qualities quite well:


Which someone has made available on its own here

Which I hosted via a python server, then merged that with the other python server using flask Blueprint.

Involving lots of temporary files, and this string of pixels from moodbar:

# Create image from pixel values
width = len(pixel_values) // 3
height = 1
image = Image.frombytes('RGB', (width, height), pixel_values)

Then the frontendistry:

        <soundbox ...>
{#if cuelet.moodbar}
<moodbar class="liner"
style="background-image:{`url(${cuelet.moodbar})`}"
/>
<moodbar class="liner mask"/>
{/if}

With styles:

soundbox {
            ...
position: relative;
}
        ...
.liner {
width:100%;
height:100%;
position:absolute;
display:block;
}
moodbar {
background-size: contain;
filter: blur(3.14159px);
}
moodbar.mask {
background: url(vertical_mid_fade.webp);
mix-blend-mode:soft-light;
}

Here is vertical_mid_fade.webp, which helps it look more mineral:


Comes out like this:


Loooovely. More of this definitely.
The user may need all these glowy features disabled if it bogs them down...
Anyway!

Refactoring goes awry

I split from Cueleter a Cuelet, but the move from:
class SyncableCueleter {
    ...
sync_cuelets(playlets) {
        ...
cuelet = {in:playlet.in,out:playlet.out}
To
cuelet = new Cuelet({
orch:this,
in:playlet.in,out:playlet.out
})
Has left those <soundbox> (representation of each cuelet) saying "!buffer" and without moodbar, as if it no longer knows about properties on cuelet now that it isn't simply a hash.

A day later, I hear that:

You can also use $state in class fields (whether public or private):

class Todo {
done = $state(false);
text = $state();
        ...

So perhaps Cuelet needs to $state() for its buffer|moodbar properties:
class Cuelet {
public orch:Cueleter
public in
public out

buffer = $state()
moodbar = $state()
constructor(opt) {

As as is usual in Svelte, the compiler does half of the education:
    We must rename cuelet.ts to cuelet.svelte.ts, to be verbose about where Svelte can apply itself.

And yes. We are back where we were again.

npm update

Between svelte-5.0.0-next.85 and -next.131 they added live code updates! No more full page reload + wasm downloads while tweaking the UI:


Seems more flickery? Would make a nice transition - warm analogue glitch sputtery ray bendings.

This goes away when I resolve:

It says SYNC CUELETS a whole lot, unless we:
async get_moodbar() {
if (untrack(() => this.moodbar)) return
...and the same for this.buffer

Faster Selection edge sampling

Faster Knob

In KnobTime.svelte:

let {
value=$bindable(5),
// 20ms at a time
step=0.02,

...props
} = $props()

Zone's selections and files

We now create a Fili to load an input file.

They "happen" in a <File {fil}...

When ready, they spawns an associated Sele, which continue what sel used to do:

{#each selections as sel (sel.id)}
<Selection {sel} ... />
{/each}

Where we initialise it into...

let in_time:tracktime = $state(sel.in != null ? sel.in : 30)
let out_time:tracktime = $state(sel.out != null ? sel.out : 36)

So they can be changed by knobs, which reactively leads into:

$effect(() => {
// inclusively select dublet spaces
let fel = {
in: Math.floor(in_time / chunk_length) * chunk_length,
out: Math.ceil(out_time / chunk_length) * chunk_length,
}
if (fel.in != sel.in || fel.out != sel.out) {
// non-reactively set it here
sel.set(fel)
console.log("Selection Woke",sel)
// then cause a reaction
// < only needed when adjusting sel.out, wtf?
on_reselection()
}

This could probably be two lines, but see needed when adjusting sel.out

I will hopefully find time to minimise a bunch of this confusion, I'll just pass it along for now.

eg Sele's in and out don't need to be $state, but playlets does:

export class Sele {
public id
public fil:Fili

public in
public out
public playlets:adublet[] = $state([])
public modes

Anyway, when compiling this selection to playlets back up in Zone:

// generate a bunch of tiles for your ears to walk on
function make_playlets(sel:Sele):adublet[] {
// how to encode (modes)
sel.modes = clone_modes()
// and attaches the Fili's identity
if (!sel.fil.dig) debugger
set_modes_value(sel.modes,'input',sel.fil.name+"#"+sel.fil.dig)

We identify the fil in sel.modes as if it was another option to ffmpeg.

We shall move to something like this soon, when we think about syncing files to the worker and making that worker further away (via http)

sel then branches into nublets:

sel_to_modes(nublet,nublet.modes)
// this now describes a unique dublet
nublet.modes_json = JSON.stringify(nublet.modes)

So we can find things in the cache:

let ideal = dublets.find(
dublet => dublet.modes_json == nublet.modes_json
)

There is a vague level of matching too:

let vague = dublets.find(
dublet => dublet.in == nublet.in && dublet.out == nublet.out
&& dublet.sel.fil == nublet.sel.fil
)

Which would have it play whatever it has for that file+time, eg if you change the desired bitrate

This seems a little extraneous. Could we just play the input file? Read on...  

jammed in the Cuelet

I drop a file in and the cuelets fail to render the new audio!

If I widen the loop to get it to render a never-before-cached cuelet time, the new audio is used:


After headscratching, I put in this delete when syncing a new objectURL:
 
class Cuelet {
...
sync_cuelet(playlet:adublet) {
    ...
// find playable
let dublet = playlet.ideal_dub || playlet.vague_dub
if (!dublet) return
if (this.objectURL != dublet.objectURL) {
delete this.buffer

Since of course:

async decodeAudio() {
if (untrack(() => this.buffer)) return

We are now at f2e87c03bbba1: delete buffer when syncing a new objectURL

Horizontal Knob

In Knob:

let {
        ...
axis = "Y",
        ...
} = $props()

And at some point:

function get_movement(event:PointerEvent) {
let key = "movement"+axis
if (event[key] == null) throw "no such axis: "+axis
let movement = event[key]
// towards the top of the screen decreases Y
if (axis == "Y") movement *= -1
return movement
}

Then in KnobTime

<Knob
...
axis="X"

bind:grabbed={grabbed}
...
/>

{#if grabbed}
<aro>⇔</aro>
{/if}

Also if you look at Selection,

    the knobs are snippeted to Schaud for positioning on the ends of the cuelets

    and then positioned a little more to mark their exact values on the cuelets timeline!

<Schaud {needle_uplink} {sel} {on_reselection}>
{#snippet leftend(cueletsin:tracktime, width_per_s)}
...
<span>
<grit class="openbracket"
style="left:{-locator_grit(cueletsin,selin,width_per_s)}px">
<KnobTime
bind:value={selin}
{commit} >
{#snippet label()}
in
{/snippet}
</KnobTime>
</grit>
</span>
{/snippet}

Using:

function locator_grit(fromtime,totime,width_per_s) {
let delta = fromtime - totime
return delta * width_per_s
}

This looks like this:


There is the issue of your pointer not appearing where the knob ends up.

Perhaps we should slide cuelets along and leave the in-point still, like the slip tool in video editing? 

Then the...same for out-point? 

Perhaps if all the ui was squishy slotty constellations of stuff, wide angle lensing on the focus... 

The set of beads (cuelets) increasing in frequency as further bits are hauled in from the shoulder, your hand swooshing the sound sideways.

Speedy Knob

I get back to accommodating hi res time as in|out.

The inclusively select dublet spaces moved in here (from before)

export class Sele {
    ...
// receives fast and fine time adjustments
on_adjust(finely) {
this.in = finely.in
this.out = finely.out
// inclusively select dublet spaces
let fel = this.inclusivise()
if (fel.in != this.in_inclusive || fel.out != this.out_inclusive) {
this.in_inclusive = fel.in
this.out_inclusive = fel.out
console.log("Selection Woke",fel)
// then cause a reaction
this.enc.on_reselection()
}
}
    ...
get_timespace() {
// we have a notch
let length = this.out_inclusive - this.in_inclusive
if (isNaN(length)) throw "NaN"
let chunk_length = this.enc.chunk_length
let n_chunks = Math.ceil(length / chunk_length)
return {length,n_chunks,chunk_length}
}

Before, get_timespace() used this.in - which should have probably caused the joblet runner to react, but for some reason didn't?

The Cuelet 's localise_time() also needed to switch to this.in_inclusive - and thus stops many spurious calls to sync_cuelet() while we are cranking knobs, instead just when this.in_inclusive changes what playlets exist, or the playlets themselves mutate.

All this connection emerges when Selection.svelte does:

$effect(() => {
sel.on_adjust({in:in_time,out:out_time})
})

They transmit to Schaud.svelte

// respond to editing
let ori = modus[1] = new ModusOriginale()
$effect(() => {
sel?.in != null && ready && ori.edge_moved({which:'in'})
})
$effect(() => {
sel?.out != null && ready && ori.edge_moved({which:'out'})
})

And then ModusOriginale

edge_moved = ({which}) => {
if (this.edge_moved_recently()) return
        ...
// this one time we play this sound
this.zip = new Ziplet({orch:this.orch, mo:this, fil:this.fil})
Object.assign(this.zip,{playFrom,playFor})
this.zip.start()
let is = this.zip

let declack = 0.01
// replaces the old Zip
was && fadeout(was,declack)
fadein(is,declack)
// mutes the other Modus
let others = this.orch.modus.filter(mo => mo != this)
others.map(o => fadeout(o,declack))
// then after a while
let thence = playFor-declack
setTimeout(() => {
if (is != this.zip) return
fadeout(is,declack)
others.map(o => fadein(o,declack))
setTimeout(() => {
delete this.zip
},declack*1000)
},thence*1000)
}
// debounce eventing for in and out when sel is moved
public last_edge_moved_ts:unixtime
edge_moved_recently() {
let was = this.last_edge_moved_ts
this.last_edge_moved_ts = now()
let delta = this.last_edge_moved_ts - was
if (delta < 0.006) return 1
}

Most of that's pretty ugly for lack of abstraction of pushing a new soundtrack into the moment, aesthetically.

There are details in reality, like this bit of class Ziplet is now :

// selection trims
get duration():number {
if (this.playFor) {
// this is the duration when trimming the end
// it already knows about any playFrom
return this.playFor
}
let dur = this.whole_duration
if (this.playFrom) {
dur -= this.playFrom
}
return dur
}

Yet this didn't need to change:

get ends_at() {
if (this.startTime == null) throw "!startTime"
return this.startTime + this.duration
}

And here's a look at precise looping:



Thanks!



Other Projects with media in the browser

https://martinwecke.de/108/

a drum machine with a nice simple interface.

https://github.com/bwasti/mebm

browser based video editor that supports animation of images and text overlays

Other Stuff

educational material for teaching and learning Fundamentals of Music Processing (FMP) with a particular focus on the audio domain. Covering well-established topics in Music Information Retrieval (MIR) as motivating application scenarios, the FMP notebooks provide detailed textbook-like explanations of central techniques and algorithms in combination with Python code examples that illustrate how to implement the theory.

Similar Music Finder - gemtracks.com

For library expansion.

 "The society that separates its scholars from its warriors will have its thinking done by cowards and its fighting done by fools."

Thursday, 23 May 2024

NZ Society

increasing non-attendance in schools over the last 10-15 years

From a Reddit thread, TW dramatic visions of malcontents

"Parents simply didn't believe you could close schools for months and then go back to insisting that every day of school was essential."

If those parents are anything like me they're seeing that the social contract has been broken, it's dog eat dog, money over everything, and we're one more toilet paper shortage away from societal collapse.


One of my sons is a poster child for the problem....

* Repeated absences. Often I wasn't even told he was absent.

* Desperately, curl up naked on a icy cold winter bathroom floor rather than climb in shower and go to school, level of unhappy.

* Totally lost in many subjects, but got no help from a high decile school.

* Still can't count his change.

If I had a do over, I wouldn't have sent him to school at all.

All it did was permanently damage him, especially his self esteem, and my relationship with him and taught him nothing.

I now have absolutely totally zero respect for the NZ education system.

If you fit nicely in their box like my other children did. Sort of OK. Not good, but at least they got a little bit out of it.

If you don't.

It is literally soul destroying, for way less than zero benefit.

We go on and on and on and on about mental health and youth suicide.... but when it comes to facing the source and fixing the education system.... we're all talk and zero do.

Sure we hand out psychiatric medicine like lollies.... but address the dysfunction in society....

Nah Escitalopram is much cheaper.


Giving up your energy, joy, and libido to better handle being forced to do shit you don't want to do has never felt like a very good deal.

I wagged 3 months of 6th form in the 90’s and still passed in the top 10% of the class, that’s how memorisation focused it was back then, and I hear little has changed.

It’s antiquated knowledge that in no way prepares kids for the real world imho. In many ways it’s a complete hindrance.

 

It now takes both parents working full time to be able to afford a relatively modest life.

Our children are being raised by strangers, and yet no one seems to think this is a problem.


We won't have anything to teach men who to process emotions but we'll spend a ton (and not enough ironically) about what to do when a man has bashed you, his partner.


They still need an education, and it's bizarre to suggest they don't

No one suggested that. That's your little addition, without which you would have no response to make.

Ah I see you're one of those people that can only handle one-point replies, otherwise you just pick the most convenient point and reply to that

... 

Not at all surprising. My daughter was a high achiever at primary and intermediate but hates high school. She says that the teachers don't respect the kids, so the kids don't respect the teachers, and that what they are learning doesn't feel relevant.


What Would You Need A Brain For


Just a reminder about the Tui Oil Field.

So tell me Mr. Jones how does operations like this benefit our environment and communities? 


Jones says: "Some people argue against minerals extraction, but gladly rely on the conveniences of modern society that are enabled by those resources"

Is this really the level we're on? The idea we should simply do the bad thing on purpose KNOWING that it's bad... just to spite people the govt percieves as hypocritical??


ACT's Resources spokesperson Simon Court says: "A high-wage economy depends on affordable and reliable energy. In other words, we need to drill, baby, drill."

I've noticed that the way this government talks about mining is almost sexual, They can't hide their lust and glee for those RICH ENDOWMENTS, that land OVERFLOWING with minerals, just WAITING to be DRILLED, baby. Like 🤮🤮🤮


From Russia Waft Twats

Also somewhere in a world news thread:

there's a reason there were so many economic ties with russia when the war started. basically for the last 30 years, the west has basically been trying to hug russia into international cooperation, and while a fair number of us thought we were succeeding for a while, they were mostly just biding their time while they got their tentacles into our ....let's say more easily influenced parties.