Closed dpwe closed 6 months ago
(See https://github.com/bwhitman/amy/tree/junopatches branch for our latest stabs at this) I've added a load_patch
AMY param that does the work of loading the AMY strings from ROM and have updated amy_patches.py
to generate these for DX7 and Juno for now. The patch loader uses the osc
you loaded the patch on as base_osc
and sets the oscillators relative to that osc. This doesn't yet do anything about polyphony.
As of right now, in the junopatches
branch, you can do (python, but C structs from e.g. Arduino work too)
import amy; amy.live()
amy.send(osc=0, load_patch=1) # setup patch 1 (a juno patch) to start at osc 0
amy.send(osc=0, vel=1, note=50) # play a note on that patch
amy.send(osc=5, load_patch=1) # load the same patch on osc 5 -- because I know that juno patches are 5 oscs
amy.send(osc=10, load_patch=1) # load the same patch on osc 10 -- ....
# play a chord
amy.send(osc=0, vel=1, note=50)
amy.send(osc=5, vel=1, note=54)
amy.send(osc=10, vel=1, note=58)
This seems to work well. "patches" 0-127 are Juno patches and 128-255 are DX7 patches. But this presumes the user knows that Juno patches take X space, dx7 takes Y (9), other types of future patches will be unknown.
@dpwe's proposal of a clone
command would help a lot. We could do, for ex:
amy.send(load_patch=1, osc=0, clone=4) # set starting osc to 0 but make 4 copies, inherently knowing how many oscs each patch takes
But then, how do we send note ons, parameter changes, etc, to individual "voices" ?
(I should also point out the new capabilities for "recording" patches, in Python, used by amy_headers.py
to generate patches.h
for Juno and DX7:
amy.log_patch() # starting logging to a patch
amy.send(...) # do whatever
patch = amy.retrieve_patch() # returns a patch string
A proposal for voices vs oscillators is simply to have this clone
become a new parameter called voice
, which is a comma separated string of integers. Let's say there are 32 of them total. We keep a map of base_osc <-> voice_idx
in the AMY kernel and update it when they're instantiated via patches. Only patches can make a voice. If you want a simple sine wave to be a voice, make it a patch. (Maybe we can later allow for loading of patches over AMY runtime instead of forcing them to be baked in.) So,
amy.send(load_patch=1, voices="0,1,2,3") # load juno patch into 4 voices. internally figure out which oscs to use and update our internal map of oscs <-> voices
amy.send(load_patch=130, voices="4,5") # load dx7 patch into 2 voices.
amy.send(voices="0,1", vel=1, note=40) # send this note on to voices 0 and 1
amy.send(voices="2",vel=1, note=44) # different note on differnet voice
amy.send(voices="4", vel=1, note=50) # dx7 note on
Still TBD:
This is great. voice
is a logical mapping to a block of oscillators, distinct from osc indexing, and dynamic. You can send Amy commands with both voice
and osc
parameters, so amy.send(voice=4, osc=1, filter_freq="440,0,0,0,5")
means to set the filter frequency of the second (i.e, index of 1) oscillator in the set of oscillators assigned to voice 4, which is maybe a Juno patch. You have to know which oscillator does what within a particular patch, but that's what documentation is for.
Also, I love the way you said kernel.
I'm working on prototyping voices
now in the junobranches
branch. It may get annoying. e.g.
amy.send(voices="0,1",load_patch=1) # loads juno patch 1 (5 oscs each) into voices 0 and 1 (oscs 0-4 are voice 0, oscs 5-9 are voice 1)
amy.send(voices="2", load_patch=130) # loads DX7 patch 2 (9 oscs each) into voice 2 - oscs 10-18 are voice 2
amy.send(voices="0", load_patch=131) # load dx7 patch 3 into voice 0, but at 9 oscs it will eat into the oscs used for voice 1
or
amy.send(voices="0,1",load_patch=1) # loads juno patch 1 (5 oscs each) into voices 0 and 1.
amy.send(voices="3", load_patch=130) # loads DX7 patch 2 into voice .. 3? how do I know which oscs to use?
A way to get around this for now is to force a full amy.reset() if you do anything with voices that is non monotonically increasing and sequential. that is, make the user know ahead of time how they want to map out voices and if they go back and change it they'll have to start over again.
But the real way is to keep the oscs set up as non-consecutive blocks that any voice can borrow from and we keep a map of (used/unused) per oscillator. And when I need to grab, say 7 oscillators, I scan through that list and piece it together. The voice map is more like a linked list of oscillator indices , not base_osc and count. Then we'll need to run DOS defrag on it every so often, so....
A middle ground is to maintain a map of available oscillators, but to require allocations to be contiguous (like malloc() works). With fragmentation, you might be unable to allocate a new voice before you completely run out of oscillators, but that's not the end of the world - if you've been allocating and deallocating voices, maybe you want to do a reset() now and then.
OK, that's how it's working now, will only alloc contiguously.
e.g.
amy.send(voices="0,1", load_patch=0) # load juno patch on voice 0&1, using oscs 0-4 and 5-9
amy.send(voices="2", load_patch=129) # loads dx7 patch on voice 2, will use osc 10-19
amy.send(voices="0", load_patch=1) # will replace the voice 0 with another juno patch. since same osc count reuses osc 0-4
amy.send(voices="0", load_patch=130) # can't fit 9 in osc-0-4, so uses 20-29. osc 0-4 are now free though
This is all working AFAICT in junopatches
.
I feel like this is in a good spot for a version 1. The load_patch
happening at message receive time and not scheduled time is a known issue but may just be something we live with. We can create new issues as we see them.
Just to say, the Juno patches are still somewhat in flux (changes to the interpretation of midi values in juno.py). This is about achieving ear-equivalence with recordings of a real juno-106. I know this doesn't matter for most users.
AMY specified low-level control over individual oscillators. Useful synthesizer tones generally involve multiple oscillators configured to act together. In analog synthesizers, these are oscillators slightly detuned or in octaves, to add harmonic complexity. In FM, there is a very specific use of one oscillator as the the (phase modulation) input to later oscillators. But in all cases, the overall "note design" and note events involve multiple, co-ordinated oscillators.
There's a need, then, to be able to configure multiple oscillators together, yet AMY commands typically affect only a single oscillator. The concept of a "patch", however, is a set of parameter settings with an overall, global result. We use built-in patches to configure FM voices to provide complete configurations inherited from the DX7 using the
wave=ALGO, patch=<n>
Python-interface idiom. They effectively set a lot of parameters of 6 or 9 individual oscillators.We are also introducing presets to emulate the subtractive-synthesis Juno synthesizer, which also involve a lot of parameters for 4 or 5 individual oscillators. Unlike the FM/DX7, we have not yet 'baked' these presets into the AMY engine, but it would be nice to have them immediately available.
One way to do this is to package up the oscillator configuration, for instance by recording and storing the wire messages. This is a neat, agnostic way to capture any broad set of lower-level AMY parameters. However, for a polyphonic synthesizer, you need to configure multiple blocks of AMY oscillators, one block for each simultaneously-sounding voice. An exact recording of a particular set of wire-level configuration commands will only describe the setting of a single set of oscillators. If we wanted to provide 6 simultaneous voices, we'd have to store distinct config sets for each voice.
Messages (other than for global effects like chorus and EQ) start with an oscillator specification (
osc=<n>
in the Python wrapper, orv<n>
in the wire protocol). A complete patch, however, may include interdependencies between oscs: There is the modulation oscillator sourcemod_osc=<n>
, the secondary oscillators used in an FM patchalgo_source=a,b,c,...
, and the newchained_osc
concept that propagates note-on events along a chain of oscs.One possibility is that we freeze patches that set up co-ordinated groups of oscillators in a fixed oscillator-address range, e.g. oscs 0 to 9. We then support a different command (perhaps
clone_osc
will suffice?) that can then duplicate this group to a different set of oscillators, including fixing up all the references to other oscillators. Essentially, this just means allowingclone_osc
to clonemod_osc
,algo_source
, andchained_osc
, but to always interpret them as relative to osc 0.