musikinformatik / SuperDirt

Tidal Audio Engine
GNU General Public License v2.0
519 stars 75 forks source link

Samples lazy loading #238

Closed ndr-brt closed 3 years ago

ndr-brt commented 3 years ago

Would be nice to have the possibility to configure superdirt to load samples lazily. E.g. a dictionary with paths and info could be loaded in memory at startup, but the sample audio data could be loaded at the first event.

This would permit to improve performances especially on dated hardware.

telephon commented 3 years ago

Good idea, but takes a bit of work.

yaxu commented 3 years ago

This is how 'classic dirt' works, a good option for low spec hardware.

bgold-cosmos commented 3 years ago

Yes, in fact before SuperDirt, the lazy-loading behavior of Dirt meant that the first time any longish sample played there would be a noticeable stutter while Dirt accessed the disk. Which led people to invent workarounds to force Dirt to pre-load a bunch of samples before starting a livecoding session!

IME on Raspberry Pi setups, "disk" access is still usually quite slow so I'd rather load all samples at startup, and just be careful about curating the directories loaded if RAM usage is an issue. Typically you can get a few hundred samples within 512 MB, so I've found 1GB total RAM (like on a Pi 3b) is ample enough.

ndr-brt commented 3 years ago

It would be nice that this behaviour could be configured at startup.

telephon commented 3 years ago

I've written up a solution that is almost done, but it makes things a little bit more complicated. Wouldn't it be better if you could send a message from tidal to make superdirt load a small subset? This can be done anytime on the fly.

ndr-brt commented 3 years ago

Yes, that can be a way to avoid the "stuttering" (it will happen also in superdirt?), but a fallback behavour like: "the sample is not loaded in memory, let's load it right now" should be needed

telephon commented 3 years ago

You can try that branch: https://github.com/musikinformatik/SuperDirt/tree/topic-read-on-demand

telephon commented 3 years ago

I forgot to say: with that branch, you can put this in your startup file, before you load the samples:

~dirt.soundLibrary.doNotReadYet = true;

This switches it on. When a sound is used first, it will post a message and read in the background.

ndr-brt commented 3 years ago

@telephon it works! The only issue is that the sample is not played on the first event but only after the second one.

telephon commented 3 years ago

The only issue is that the sample is not played on the first event but only after the second one.

Well that would be magic, wouldn't it?

yaxu commented 3 years ago

Classic dirt would keep samples in a queue until they were loaded, sometimes late but often inside the latency deadline for shorter samples. Looking at the code there is a flag to not play late events but it isn't actually used anywhere. There are multiple threaded 'workers' for loading samples, I'm not sure if that really speeds things up (I didn't write that bit). In practice the first time you used a large number of samples at once there would be some time glitching.

telephon commented 3 years ago

I see, yes, we could try, and given a large enough latency setting it may even work sometimes.

telephon commented 3 years ago

…I've changed it, give it a try

ndr-brt commented 3 years ago

that's perfect, everything gets loaded without glitches, also the famous bev sample!

ndr-brt commented 3 years ago

my supercollider latency is set at 0.3

ndr-brt commented 3 years ago

no, wait, it happened that, when load a sample, the server disconnected:

Buffer UGen: no buffer data
Server 'localhost' exited with exit code 0.
server 'localhost' disconnected shared memory interface
ndr-brt commented 3 years ago

I noticed that this error appears only when I trigger a sample with once function from tidal

telephon commented 3 years ago

Hm, interesting, this may point to a bug in the server, let's try and find it.

It could be easily caught, but that would mean an extra check for every buffer event, I'd like to save the electricity.

telephon commented 3 years ago

@ndr-brt The connection with once seems spurious. Maybe you can trace it down further?

yaxu commented 3 years ago

once does work a bit differently on the tidal side, missing out the scheduler - so it's feasible the 'artificial latency' calculation is buggy/different

telephon commented 3 years ago

I see. But even if the buffer isn't ever loaded, the server shouldn't crash.

telephon commented 3 years ago

I can reproduce it directly, will try and find a good reproducer.

ndr-brt commented 3 years ago

It's easy to reproduce, emitting a lot of events with samples not yet loaded:

d1 $ n (segment 38 $ irand 38) # s "diphone"

why don't allocate an empty buffer when a doNotReadYet buffer object is instantiated?

telephon commented 3 years ago

I've tried it out with a version that never loads any buffers. The problem seems to be that sometimes, when you send messages that post an error in sclang before the server has initialised the memory interface properly, then the server crashes. So if you wait till the server has booted it works. I've pushed a fix, please try!

ndr-brt commented 3 years ago

the problem persists

telephon commented 3 years ago

thanks for trying, unfortunately I can't reproduce it any more.

When you start supercollider with superdirt and then wait for a little moment until the server is booted, and then begin to make sound, does it still happen?

ndr-brt commented 3 years ago

Yes, it still happens, also after a minute or two. There's a way to enable stack trace console print?

telephon commented 3 years ago

there are cmake flags, described in the readmes here https://github.com/supercollider/supercollider.

a question: does this code crash the server as well?

(
s.reboot {

    fork {
        SynthDef(\test, { |out, bufnum, sustain = 1, begin = 0, end = 1, speed = 1, endSpeed = 1, freq = 440, pan = 0|

            var sound, rate, phase, sawrate, numFrames;

            // playback speed
            rate = Line.kr(speed, endSpeed, sustain) * (freq / 60.midicps);

            // sample phase
            // BufSampleRate adjusts the rate if the sound file doesn't have the same rate as the soundcard

            numFrames = BufFrames.ir(bufnum);
            sawrate = rate * BufSampleRate.ir(bufnum) / (absdif(begin, end) * numFrames);
            phase = (speed.sign * LFSaw.ar(sawrate, 1)).range(begin, end) * numFrames;

            sound = BufRd.ar(
                numChannels: 2,
                bufnum: bufnum,
                phase: phase,
                loop: 0,
                interpolation: 4 // cubic interpolation
            );

            Out.ar(out, sound)
        }).add;

        100.do { 

            Synth(\test, [\bufnum, 4711]);

            0.03.wait
        };
    }

}
)
ndr-brt commented 3 years ago

@telephon nope, a lot of Buffer UGen: no buffer data but the server does not crash

telephon commented 3 years ago

I've found the culprit: a division by zero in the playback synth when NumFrames is 0. Can you check the updated branch again? Thanks!

yaxu commented 3 years ago

To be honest I don't know why we can't define x / 0 = 0 to solve this (and also round pi to 3.1)

telephon commented 3 years ago

I also think we should solve the underlying problem first and make 0 = 1 – after all, also zero is something, and it is being treated unfairly.

yaxu commented 3 years ago

Yes or just exclude both 0 and 1 from the class of numbers, and start at 2. I think this is how Euclid did things in the elements. I had a go at implementing that here https://github.com/Kairotic/weavingcodes/blob/master/types/Dyadicv2.hs

telephon commented 3 years ago

And if you count down further, you go 4, 3, 2, -2, -3, …

ndr-brt commented 3 years ago

@telephon it's working really well! I tested a lot, no problems encountered, no glitches, no delays... perfect :ok_hand: thanks

ndr-brt commented 3 years ago

as noted in https://club.tidalcycles.org/t/superdirt-lazy-samples-loading/3148/8, when a sample does not exists there's an uncatched error printed in console:

CALL STACK:
    DoesNotUnderstandError:reportError
        arg this = <instance of DoesNotUnderstandError>
    Nil:handleError
        arg this = nil
        arg error = <instance of DoesNotUnderstandError>
    Thread:handleError
        arg this = <instance of Thread>
        arg error = <instance of DoesNotUnderstandError>
    Object:throw
        arg this = <instance of DoesNotUnderstandError>
    Function:protect
        arg this = <instance of Function>
        arg handler = <instance of Function>
        var result = <instance of DoesNotUnderstandError>
    Environment:use
        arg this = <instance of Event>
        arg function = <instance of Function>
        var result = nil
        var saveEnvir = <instance of Environment>
    DirtEvent:play
        arg this = <instance of DirtEvent>
    OSCFuncRecvPortMessageMatcher:value
        arg this = <instance of OSCFuncRecvPortMessageMatcher>
        arg msg = [*11]
        arg time = 190.65873981826
        arg addr = <instance of NetAddr>
        arg testRecvPort = 57120
    OSCMessageDispatcher:value
        arg this = <instance of OSCMessageDispatcher>
        arg msg = [*11]
        arg time = 190.65873981826
        arg addr = <instance of NetAddr>
        arg recvPort = 57120
    Main:recvOSCmessage
        arg this = <instance of Main>
        arg time = 190.65873981826
        arg replyAddr = <instance of NetAddr>
        arg recvPort = 57120
        arg msg = [*11]
^^ The preceding error dump is for ERROR: Message 'at' not understood.
RECEIVER: nil
telephon commented 3 years ago

should be fixed now. Can you check again?

ndr-brt commented 3 years ago

perfect!

ndr-brt commented 3 years ago

@telephon seems like synthdefs are not working now, I didn't noticed it before

ERROR: Non Boolean in test.
RECEIVER:
   nil

PROTECTED CALL STACK:
    Meta_MethodError:new    0x55f9479008c0
        arg this = MustBeBooleanError
        arg what = nil
        arg receiver = nil
    Object:mustBeBoolean    0x55f9468dc540
        arg this = nil
    DirtSoundLibrary:getEvent   0x55f94870ca00
        arg this = a DirtSoundLibrary
        arg name = kk
        arg index = none
        var allEvents = nil
        var event = ( 'instrument': kk, 'hash': 424851966 )
    DirtEvent:mergeSoundEvent   0x55f9477fba40
        arg this = a DirtEvent
        var soundEvent = nil
    a FunctionDef   0x55f9477faf40
        sourceCode = "<an open Function>"
    a FunctionDef   0x55f948cff680
        sourceCode = "<an open Function>"
    Function:prTry  0x55f947ba5c40
        arg this = a Function
        var result = nil
        var thread = a Thread
        var next = nil
        var wasInProtectedFunc = false

CALL STACK:
    MethodError:reportError
        arg this = <instance of MustBeBooleanError>
    Nil:handleError
        arg this = nil
        arg error = <instance of MustBeBooleanError>
    Thread:handleError
        arg this = <instance of Thread>
        arg error = <instance of MustBeBooleanError>
    Object:throw
        arg this = <instance of MustBeBooleanError>
    Function:protect
        arg this = <instance of Function>
        arg handler = <instance of Function>
        var result = <instance of MustBeBooleanError>
    Environment:use
        arg this = <instance of Event>
        arg function = <instance of Function>
        var result = nil
        var saveEnvir = <instance of Environment>
    DirtEvent:play
        arg this = <instance of DirtEvent>
    OSCFuncRecvPortMessageMatcher:value
        arg this = <instance of OSCFuncRecvPortMessageMatcher>
        arg msg = [*11]
        arg time = 26.461100750603
        arg addr = <instance of NetAddr>
        arg testRecvPort = 57120
    OSCMessageDispatcher:value
        arg this = <instance of OSCMessageDispatcher>
        arg msg = [*11]
        arg time = 26.461100750603
        arg addr = <instance of NetAddr>
        arg recvPort = 57120
    Main:recvOSCmessage
        arg this = <instance of Main>
        arg time = 26.461100750603
        arg replyAddr = <instance of NetAddr>
        arg recvPort = 57120
        arg msg = [*11]
^^ The preceding error dump is for ERROR: Non Boolean in test.
RECEIVER: nil
telephon commented 3 years ago

Ah yes, I see. This works again, now.

cleary commented 3 years ago

@telephon @ndr-brt , once again thanks a lot for this

I just tried a midi mapping (midi0) to external synths and got the same uncaught errors as were happening above

From my startup.scd:

        (
            MIDIClient.init;

            ~midithroughport0 = MIDIOut.newByName("Midi Through", "Midi Through Port-0");
            ~midithroughport0.latency = 0;
            ~dirt.soundLibrary.addMIDI(\midi0, ~midithroughport0);
        )
telephon commented 3 years ago

@cleary thank you, could you make sure you have the newest version? Doing git log you should have commit 064d0ac835537124ea55c7acb50a49bb3d6c3612 in the top line.

If the error still occurs, can you post the error message? I expect it to be slightly different.

cleary commented 3 years ago

@telephon confirmed I have the correct commit on the top line

The error still occurs (to note, if I just comment the doNotReadYet line in startup.scd, using midi0 does work on the same develop branch):

ERROR: Non Boolean in test.
RECEIVER:
   nil

PROTECTED CALL STACK:
    Meta_MethodError:new    0x564a247cbf00
        arg this = MustBeBooleanError
        arg what = nil
        arg receiver = nil
    Object:mustBeBoolean    0x564a24329640
        arg this = nil
    DirtSoundLibrary:getEvent   0x564a251513c0
        arg this = a DirtSoundLibrary
        arg name = midi0
        arg index = none
        var allEvents = [ ( 'hash': 1325261192, 'midiOutNotFoundError': a DirtPartTimeError, 'play': a Function, 'midiout': a MIDIOut ) ]
        var event = ( 'hash': 1325261192, 'midiOutNotFoundError': a DirtPartTimeError, 'play': a Function, 'midiout': a MIDIOut )
    DirtEvent:mergeSoundEvent   0x564a2478f080
        arg this = a DirtEvent
        var soundEvent = nil
    a FunctionDef   0x564a2478e580
        sourceCode = "<an open Function>"
    a FunctionDef   0x564a2556d380
        sourceCode = "<an open Function>"
    Function:prTry  0x564a24a629c0
        arg this = a Function
        var result = nil
        var thread = a Thread
        var next = nil
        var wasInProtectedFunc = false

CALL STACK:
    MethodError:reportError
        arg this = <instance of MustBeBooleanError>
    Nil:handleError
        arg this = nil
        arg error = <instance of MustBeBooleanError>
    Thread:handleError
        arg this = <instance of Thread>
        arg error = <instance of MustBeBooleanError>
    Object:throw
        arg this = <instance of MustBeBooleanError>
    Function:protect
        arg this = <instance of Function>
        arg handler = <instance of Function>
        var result = <instance of MustBeBooleanError>
    Environment:use
        arg this = <instance of Event>
        arg function = <instance of Function>
        var result = nil
        var saveEnvir = <instance of Environment>
    DirtEvent:play
        arg this = <instance of DirtEvent>
    OSCFuncBothMessageMatcher:value
        arg this = <instance of OSCFuncBothMessageMatcher>
        arg msg = [*13]
        arg time = 57.22197841946
        arg testAddr = <instance of NetAddr>
        arg testRecvPort = 57120
    OSCMessageDispatcher:value
        arg this = <instance of OSCMessageDispatcher>
        arg msg = [*13]
        arg time = 57.22197841946
        arg addr = <instance of NetAddr>
        arg recvPort = 57120
    Main:recvOSCmessage
        arg this = <instance of Main>
        arg time = 57.22197841946
        arg replyAddr = <instance of NetAddr>
        arg recvPort = 57120
        arg msg = [*13]
^^ The preceding error dump is for ERROR: Non Boolean in test.
RECEIVER: nil
telephon commented 3 years ago

Ah I see, yes. This has nothing to do with midi. It happens for all synth events:

SuperDirt.default = ~dirt;
~dirt.soundLibrary.addSynth(\zeze, (instrument: \default));
(type:\dirt, orbit:0, s: \zeze).play;

I've just fixed it.