Cabbage Logo
Back to Cabbage Site

Wavetable Loading System

Hello everyone!

So the past few months, I’ve been wanting to create a wavetable oscillator like that of Serum, Surge, or Vital.

I’ve done a few hours of research, but I keep getting misleading results, so I figured I’d ask here.

I figured out how to morph between tables using the tabmorph opcode. As well as how to load audio files into tables using GEN1.

However I cannot seem to figure out how to load an audio file and morph between the individual waveforms of it.

My thoughts was that maybe I have to figure out how to “dump” the waveforms of the audio file into a table to morph between them. But I’m unsure if this is the best solution.

If anyone has a solution or could link me to an example. I’d really appreciate it :blush:

Can you can just use the ntrpol opcode?

Isn’t that what you do when you use GEN01, i.e, load a soundfile into a table?

I don’t have an example, but I’m sure one could create a nice Serum clone with Csound :wink:

I can use ntrpol or tabmorph to morph between tables.
But whenever I tried GEN1 it always plays the entire sample. (Not individual waveforms.)
Basically I want a way to morph through the waveforms of a sample with a slider.

I’m unaware if GEN1 is just loading the sample, or if it automatically dumps the waveforms as well. But I couldn’t get it working with a slider.

I found some examples here:

But some of these examples wouldn’t compile due to errors. Which is odd since Csound is so backwards compatible :face_with_raised_eyebrow:

A waveform is just a collection of samples. GEN01 hold a waveform of arbitrary length. Do you mean cycle?

Possibly? Please excuse me if I brought up the wrong term :sweat_smile:

Oddly in synthesis I keep getting misleading results due to similar words being thrown around. Csound also has a lot of opcodes regarding wavetable synthesis, so I couldn’t figure out which one I was looking for.

Basically I just want to implement a method for users to load in custom wavetables, and then use a slider to morph between the “frames” of the wavetable.

I’d thought that Figure 2.4 in the link I’d sent was the answer. But it had an error due to line not having valid arguments. I fixed it, but wasn’t able to get any audio. So I couldn’t tell if it did what I wanted to or not.

To do this you need to make sure the waveforms are going to be bandlimited when they are played back. Look at vco2init for this purpose. It will let you load any file, but will make sure no aliasing occurs.

The function table morphing opcode will work in this context, as you will already have them loaded to a table. @Retornz does this in his ToneZ synth.

And he’s taken it even further with the new version which offers even more control over your sounds. It’s all open source and very well written, and it’s only 3000 lines of code :+1: :rofl:

1 Like

I really do like ToneZ. Gave it a try the other day. And have been checking out the source code. So much inspiration in this synth! :blush:

However it only morphs between two tables doesn’t it? I guess I’m trying to do more like 256. (I think that’s what Serum and Vital do.)

1 Like

Hello!
I’ve been developing a synth similar to what you’re thinking for a few months now. The one for you, in my opinion, is the ftmorf opcode which exactly morphs between multiple ftables. the important thing is that they all have the same length in samples. http://www.csounds.com/manual/html/ftmorf.html

in my case I exported single waveform cycles directly from the serum wavetable editor at 1024 samples in length. check out this repository:


it’s still in development but already sounds pretty nice.

I hope I was helpful :smiley:

1 Like

Thank you for sharing! I’m gonna give this a download and give it a try :slightly_smiling_face:

I think I found what I was looking for in the Table3FilePlayer example.

I’m going to attempt to modify it. If I get the result I need. I’ll post the code here for everyone :+1:

1 Like

This is a really late response. But I hope it still helps someone.

I think the problem here is that for Vital, Serum etc. a wavetable is an array of what is usually a table in csound. There are some opcodes in csound like ftmorf, tabmorph which are designed to handle sounds from a mixture of tables. But they have two disadvantages: 1. They interpolate the full tables which may be an overkill if one only wants a few entries between changing from one table to another. 2. Vital has 256 frames of 2048 samples. That would require to construct 256 csound tables. Not impossible, but tedious.

I therefore came up with a method which reads Vital style wavetables into one csound table and picks the correct entries for a given frame number and sample-in-frame index. (A quasi-standard for Vital seems to be that the frames are written consecutively to a single wav file with 256*2048=524288 samples.)

<Cabbage>
form caption("Untitled") size(400, 300), guiMode("queue"), pluginId("def1")
keyboard bounds(8, 158, 381, 95)
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 --midi-key-cps=4 --midi-velocity-amp=5
</CsOptions>
<CsInstruments>
; Initialize the global variables. 
ksmps = 32
nchnls = 2
0dbfs = 1

;instrument will be triggered by keyboard widget
instr 1

kx madsr 0.001, .3, 0, 0
kframe = int(255-255*kx)
aphi phasor p4
aOut tab int(2048*(kframe+aphi)), 100

kEnv madsr .01, .2, .6, .4
outs aOut*kEnv, aOut*kEnv

endin

</CsInstruments>
<CsScore>
;causes Csound to run for about 7000 years...
f0 z
f100 0 524288 1 "primes.wav" 0 0 0
</CsScore>
</CsoundSynthesizer>

(It needs primes.wav as the wavetable.)

The disadvantage of this solution is that the scheme does not interpolate between frames and also not between samples. That could be implemented by reading two samples each from two of the frames and do a 4-point 2d interpolation.

1 Like

@RZorn
First, congrats on the ToneZ V2, that’s some impressive work!

Constructing any number of tables isn’t tedious unless one attempts to do this manually. Of course if one isn’t happy with ftmorf, which will only interpolate between adjacent samples, and prefers one of the tabmorph opcodes which allow interpolating between 2 non-adjacent tables (4 tables can be morphed simultaneously) that’s an issue.

Depending on one’s needs, a relatively simple solution for a wavetable synth is using ftsamplebank to load a folder of single cycle waveforms then interpolate with ftmorf.

The while loop constructs the wavetable. The preload instr should precede any playback instruments as it can take a fraction of a second for all the tables to be created although even on my phone .1 sec seems adequate. One just has to alter the path to the sample folder. Of course all the samples in the folder must be the same size. The code will automatically find the size of the imported files and compensate (see iSize).

The only drawback is that as ftsamplebank must allocate a specific series of table numbers care has to be taken if using tables elsewhere in a csd. Usually I prefer to let Csound choose it’s own table numbers and reference by name but that’s not an option except outside of the preload instr which should come first.

I’ve never had an issue with having to bandlimit the tables, usually the files from external/imported wavetables are already bandlimited. If not then generally a lpf rolling off above 16kHz works fine.

<CsoundSynthesizer>
<CsOptions>
 -odac -d
</CsOptions>
;==================================
<CsInstruments>

sr = 48000
ksmps = 32
nchnls = 1
0dbfs  = 1

instr preload
  ; must be > 1 as that is reserved for giWavetable
  giFirstTabNum = 2
  ; alter path to sample folder accordingly
  ; note that all imported files must be the same size
  ; they do not have to be power of 2
  iNumFiles ftsamplebank "...path to wavetable sample folder...",\
            giFirstTabNum, 0, 0, 1        
  iTabNum init 0
  giWavetable = ftgen(1, 0, -iNumFiles, -2, 0)
  while iTabNum < iNumFiles do
    tabw_i(iTabNum + giFirstTabNum, iTabNum, giWavetable)
    iTabNum += 1
  od
endin

instr play
  iSize = ftlen(giFirstTabNum)
  iMorf = ftgenonce(0, 0, -iSize, -2, 0) 
  iLen  = ftlen(giWavetable)
  kNdx  = linseg(0, p3, iLen - 1)
  ftmorf(kNdx, giWavetable, iMorf)
  aSig  = poscil(.8, 110, iMorf)
  out aSig
endin

</CsInstruments>
;==================================
<CsScore>
i"preload" 0 .1
i"play"   .1 32
</CsScore>
</CsoundSynthesizer>

Another option might be loading in a folder of files to array using the directory opcode and the using ntrpol which could be quite flexible for more advanced morphing (such as using using more than one instance of ntrpol.

Best,
Scott

Actually, @Retornz wrote and released ToneZ. :+1:

1 Like

Oops… thanks for pointing that out, guess I got my wires crossed. :woozy_face:

Guess I didn’t read carefully and the goal here is dealing with wavetables written as a single audio file.

One approach is to slice it up into individual tables which allows for easier morphing or single/randomized waveform playback/selection. In the create instr you need to know & enter the size (in samples) of each waveform in the source table.

For ex., in some of the wavetables here (there’s about 695):
https://waveeditonline.com/
you can see there are 8 rows of 8 waveforms so 64 waveforms. So if the file is 16384 samples (such as TRANSFOR & PRECIPIC) the iSize should be 256 (samples).

I tried with the primes.wav posted here (2048, which creates 257 tables) but the result was slightly clicky. I don’t know how Vital deals with interpolation internally but one solution would be to window the tables in the loop as they’re written.

; written by Scott Daughtrey 2024
; https://soundcloud.com/st-csound
<CsoundSynthesizer>
<CsOptions>
 -odac  -d -m2 -W
; -o 0_split_wave.wav
</CsOptions>
;==================================
<CsInstruments>

sr = 48000
ksmps = 32
nchnls = 2
0dbfs  = 1

; UDO to create multiple tables (wavetable)
; by slicing source audio
opcode split_wav, 0, Sio
    Sfile, iSize, iStartSlice xin
  
    iEndSlice init iStartSlice + iSize
    iSource = ftgentmp(0, 0, 0, 1, Sfile, 0, 0, 1)
    iLength = nsamp(iSource) ; num. samples in source table
    iNumTab = int(iLength / iSize) ; num. of sliced tables that will be created

    giWavetable = ftgen(0, 0, -iNumTab, -2, 0)

    iIndex init 0
    
    while iEndSlice <= (iLength) do
        giTable = ftgen(0, 0, -iSize, 2, 0)
        ftslicei(iSource, giTable, iStartSlice, iEndSlice)
        tabw_i(giTable, iIndex, giWavetable)
 
        iStartSlice += iSize
        iEndSlice   += iSize
        iIndex      += 1
    od

endop

; instr to call the UDO
instr split_wav
    Sfile = "/sdcard/Samples/TRANSFOR.wav" ; source file for table creation
    iSize   init 256 ; size of requested tables (number of samples to copy)
;   iStart is an optional parameter, defaults to 0
    iStart  init 0 ; index of source table to start reading
    split_wav(Sfile, iSize)
	chnset(iSize,"Size")
endin

instr play
    iSize = chnget:i("Size")
    iLen  = ftlen(giWavetable)
    iMorf = ftgen(0, 0, -iSize, -2, 0)
    kIdx  = linseg:k(0, p3, iLen - 1)
    ftmorf(kIdx, giWavetable, iMorf)
    aSig  = tone(poscil3(.8, 55, iMorf), 16000)
    outs(aSig, aSig)
endin

instr trig
    seed 0
    giTab = ftgenonce(0, 0, -40, -41, 30, 1, 28, .7, 31, .4, 35, .3)
    iBps  = 6
    kTrig = metro(iBps)
    schedkwhen kTrig, 0, 0, "play2", 0, 1/iBps
endin

instr play2
    iNote = duserrnd(giTab)
    iLen  = ftlen(giWavetable)
    iRand = int(random:i(0, iLen - 1))
    iTab  = tab_i(iRand, giWavetable)
    aEnv  = linseg:a(0, .004, .8, p3 - .008, .8, .004, 0)
    aSig  = poscil(aEnv, mtof:i(iNote), iTab)
    outs(aSig, aSig)
endin

</CsInstruments>
;==================================
<CsScore>
i"split_wav"  0  .1
i"play"      .1  32
i"trig"      33  32
</CsScore>
</CsoundSynthesizer>

Ex. uses:

If windowed properly and bandlimited (which could be added in the loop) it works fairly well to create wavetables from different source material, for example speech (if there aren’t large pauses) or sustained notes like a cello or pad timbre.

As an example, all timbres here are just fox.wav.

@ST_Music, thanks for sharing this, your examples are always so musical. :slight_smile:

First of all, honour whom honour is due. ToneZ was written by @Retornz. Maybe my user name is a bit similar.

Thanks to @ST_Music for the solutions. Regarding the first one: Maybe I overlooked some updates, but I am not aware of the opcode ftsamplebank. I cannot find it on http://www.csounds.com/manual/html/. In principle, it would be possible to generate 256 wav files of 2048 samples each with a small modification to the program I used to generate primes.wav. But I think that even if they are collected in a single subdirectory that may get a bit confusing. Also, there is a large overhead reading 256 files compared to reading one longer file.

The second solution is actually what I intend. (And the example is great! Amazing what you can get out of fox.wav.) The only problem I can imagine is that ftmorf calculates the interpolation for the whole slice table at k-rate. This may be more important if the slice is 2048 samples instead of 256.

On the other hand, this solution provides correct interpolation of the slices and between samples. So I implemented the interpolation into the code I posted before:

<Cabbage>
form caption("Untitled") size(400, 300), guiMode("queue"), pluginId("def1")
keyboard bounds(8, 158, 381, 95)
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 --midi-key-cps=4 --midi-velocity-amp=5
</CsOptions>
<CsInstruments>
; Initialize the global variables. 
ksmps = 32
nchnls = 2
0dbfs = 1

;instrument will be triggered by keyboard widget
instr 1

kx madsr 0.001, .3, 0, 0
kframe = 255-255*kx
kframei = int(kframe)
kframei1 = min(255,kframei+1)
kframef = kframe-kframei
aphi phasor p4
aindex = 2048*aphi
aindexi = int(aindex)
aindexi1 = (aindexi+1)%2048
aindexf = aindex-aindexi
asig00 tab int(2048*kframei+aindexi), 100
asig01 tab int(2048*kframei+aindexi1), 100
asig10 tab int(2048*kframei1+aindexi), 100
asig11 tab int(2048*kframei1+aindexi1), 100
asig0 ntrpol asig00, asig10, kframef
asig1 ntrpol asig01, asig11, kframef
;aOut ntrpol asig0, asig1, aindexf
aOut = (1-aindexf)*asig0+aindexf*asig1

kEnv madsr .01, .2, .6, .4
outs aOut*kEnv, aOut*kEnv

endin

</CsInstruments>
<CsScore>
;causes Csound to run for about 7000 years...
f0 z
f100 0 524288 1 "primes.wav" 0 0 0
</CsScore>
</CsoundSynthesizer>

This approach just calculates 3 interpolations per output sample, so 96 per k-cycle. Fully interpolating two 2048 samples splices would be 2048 interpolations per k-cycle plus 32 from the a-rate interpolation. But of course that calculation does not take into account that using compiled opcodes is faster than explicitly coding the interpolations in csound. (I was really disappointed that there isn’t even an aaa version of ntrpol.)

Concerning the clicks, I had this problem too when I did not consider that if you directly work on the unspliced table the next sample after the last in a splice is the first of the next splice. That’s why there is the modulus 2048 calculated, aindexi1 = (aindexi+1)%2048. After putting this line in, there were no more clicks from the code posted here. But that also should not happen in your version, @ST_Music, because you splice the table before and poscil3 should handle the wraparound correctly.

Just to note here that csounds.com is not the official Csound website. it’s csound.com, and you can find the ftsamplebank opcodes here

Btw, whilst nowhere near as interesting as the examples you guys are posting, I created a simple ftmorf demo using p5js and Csound. I find the visualisation quite nice :slight_smile:

Thanks for telling me about the correct documentation pages. So I was using obsolete docs for two years now. Appropriate that I post on the “Noobs” topic.

PS: I just studied the js/csound demo. I am impressed. The sound movement is great. And also it is interesting to see how this works with the js code calling the csound.

1 Like