diff --git a/README.md b/README.md index a9c05627..326337c2 100644 --- a/README.md +++ b/README.md @@ -1,140 +1,105 @@ -# edisyn -Synthesizer Patch Editor (Version 7) +![Edisyn Splash Banner](https://raw.githubusercontent.com/eclab/edisyn/master/pics/Banner.png) +# Edisyn +Synthesizer Patch Editor (Version 17) + By Sean Luke (sean@cs.gmu.edu) +I've been asked where my Patreon page is. So, sure, here's my Patreon page. -## About - -Edisyn is a synthesizer patch editor library written in pure Java. - -Edisyn presently supports: - -* Waldorf Blofeld and Waldorf Blofeld Keyboard (Single and Multi Modes) -* Waldorf Microwave II, XT, and XTk (Single and Multi Modes) - -Ultimately Edisyn will have some more written for it as well as I get time; and of course the patch -editors I can write are restricted to the synths I own and can test on! -Edisyn has no graphical interface editor system like -Ctrlr, but it's designed to make the GUI pretty easy to write (well, for me anyway). +Related projects: -At present Edisyn *only runs on OS X* (well, I've only tried it on OS X, that's its target). If you try running -it elsewhere, you're on your own. We have confirmation that it runs fine under Linux. +* [Flow](https://github.com/eclab/flow), a fully-modular, polyphonic, additive software synthesizer. +* [Gizmo](https://cs.gmu.edu/~sean/projects/gizmo/), an Arduino-based MIDI Swiss Army knife -## Installation -For the time being, to install Edisyn you need to do the following two things. - -1. Install the Java JDK. -2. Put the file `jar/edisyn.7.jar` where you like (it's Edisyn's executable). - - -## Running - -- *On OS X:* Double click on the file `edisyn.7.jar` -- *On Windows:* I don't know. Anyone? -- *On Linux:* In Ubuntu, you'll first need to change the Properties of the jar file (see the "Open With" tab) to your Java VM. Thereafter you can just double-click on the file. +## About -Edisyn should launch and either present you with a window asking what MIDI interface you want to use, or tell -you that there are no available MIDI interfaces, and that you'll need to work offline. +Edisyn is a synthesizer patch editor library written in pure Java. It runs on OS X, Linux, and Windows. +Edisyn is particularly good at exploring the space of patches. It has to my knowledge the most sophisticated set of general-purpose patch-exploration tools of any patch editor available. -### The Editor Pane +Edisyn presently supports: + +* DSI Prophet '08 +* E-Mu Morpheus and Ultraproteus (Single, Hyperpreset, and MidiMap modes) +* Waldorf Blofeld and Waldorf Blofeld Keyboard (Single and Multi Modes) +* Waldorf Microwave II, XT, and XTk (Single and Multi Modes) +* Oberheim Matrix 1000 +* PreenFM2 +* Kawai K1, Kawai K1m, and Kawai K1r (Single and Multi Modes) +* Kawai K4 and Kawai K4r (Single, Multi, Drum, and Effect Modes) +* Kawai K5 and K5m +* Yamaha DX7 Family (Single) +* Yamaha TX81Z (Single and Multi Modes) +* Korg SG Rack (Single and Multi Modes) and Korg SG Pro X (Single Mode) +* Korg Microsampler +* Korg MicroKorg +* Korg Wavestation SR (Performance, Patch, and Wave Sequence Modes) -The editor pane should be self-explanatory. There are four tabs which together cover all of the parameters of -the synthesizer. The first tab, *Oscillators and Filters*, also contains an area called *Waldorf Blofeld* (or -some other synth) which -lets you set the patch name and category, bank, number, and Device ID. The bank, number, and ID are mostly -for saving out to sysex files: whenever you upload or download patch to/from the synth, you'll be prompted -to revise those if necessary. +Edisyn has infinite levels of undo, CC and NRPN mapping and learning, offline modes, randomization, merging, nudging, hill-climbing, patch constriction, per-parameter customization, real-time parameter updates, test notes, etc. -### The File Menu +## Manual -* *New* creates a new editor pane, set to the default (the synth's Init patch setting). Note that only the frontmost editor pane will receive MIDI. So if you for -some reason set up both editor panes with the same interface, then request MIDI from one pane, then quickly -switch to the other pane, you could in theory get the MIDI sent to the other pane. So don't do that. +Edisyn has an [extensive manual](https://github.com/eclab/edisyn/raw/master/docs/manual/Edisyn.pdf) which describes how to run it, and (if you are so inclined) how to make new patch editors. -* *Load...* loads a sysex file into the existing editor pane. It's called Load instead of Open because it -doesn't open a new window. +## Install and Run Edisyn -* *Close Window* closes, well, you know, it closes, um, the window. +Edisyn is cross-platform and will run on a variety of platforms (Windows, Linux) but I am personally developing on and for OS X. I'd appreciate feedback and screenshots of it running on Windows and Linux so I can tweak things. -* *Save* and *Save As...* save to a sysex file. -* *Export Diff to Text...* saves out to a text file a line-by-line description of every parameter which is *different* -from the default Init patch setting, plus the current value it's set to. The patch value is either a string (in the -case of the Patch name) or a number from 0...127: this may not be that useful to you, but the parameter names might -be useful if you want to publish your patch on a sebsite and need to know which parameter settings to mention. +### Installation and Running on OS X +First install Edisyn from the [Edisyn.app.zip](https://github.com/eclab/edisyn/raw/master/install/Edisyn.app.zip) file located in the "install" directory. Sadly, it's a whopping 70MB because it includes the Java VM. :-( -### The MIDI Menu -Note that you can *send* a patch to the synthesizer and you can *write* a patch to the synthesizer. The former -just temporarily updates the synth's current patch memory so you can play it. The latter actually writes the -patch to an address in the synth, replacing whatever is there. +Sierra has really locked down the ability to run an application that's not from a commercial, paying Apple Developer. And I'm not one. So you will have to instruct Sierra to permit Edisyn to run. -* *Request Current Patch* asks the synthesizer to load the current patch memory into the editor. Note that on -some machines (like the Waldorf Blofeld) when the patch is loaded, the bank and patch number are invalid and will -be reset to some defaults, which might be confusing! +Let's assume you stuck Edisyn in the /Applications directory as usual. Then: -* *Request Patch...* asks the synthesizer to load a specific patch into the editor. If the synthesizer complies, -once the patch is loaded, Edisyn will then send the patch to the synthesizer. +1. Run the Terminal Program (in /Applications/Utilities/) +2. Type the following command and hit RETURN: ` sudo spctl --add /Applications/Edisyn.app` +4. Enter your password and hit RETURN. +5. Quit the Terminal Program -* *Request Merge* asks the synthesizer to load a specific patch into the editor. If the synthesizer complies, -then the patch is *merged* with the existing patch, meaning that some *percentage* of parameters in the existing -patch are replaced with the old patch. Then Edisyn will then send the patch to the synthesizer. +Now you should be able to run Edisyn. Let me know if this all works. -* *Randomize* randomizes the editor's current patch, then sends it to the synthesizer. -* *Reset* resets the editor's current patch to its initialized state, then sends it to the synthesizer. +### Installation and Running on Windows -* *Send Patch* sends the current patch to the synthesizer. This isn't actually used much since other commands -send the patch automatically. +I believe that the following should work: -* *Write Patch...* writes the patch to a given location in the synthesizer. +1. [Download and install Java 11](http://www.oracle.com/technetwork/java/javase/downloads/index.html). The JRE should work fine. -* *Change MIDI* sets or updates the MIDI interface. +2. Download Edisyn's jar file, called [edisyn.jar](https://github.com/eclab/edisyn/raw/master/jar/edisyn.jar), presently located in the "jar" directory. -* *Disconnect MIDI* disconnects the MIDI interface. +3. Double-click on edisyn.jar to launch Edisyn. -* *Send All Sounds Off* sends the All Sounds Off message to all channels. +#### Important Note for Windows User -* *Send Test Note* sends a 1/2 second test note to the primary channel. +Java versions earlier than 11 (or so) do not handle high-resolution displays properly, so Edisyn will appear teeny-tiny. You need to upgrade to 11. +Also Edisyn makes heavy use of Java preferences to store persistence information: what menu option you chose last time, what should be the default synth editor to pop up, and so on. However there is a longstanding Java/Windows bug which makes Java preferences not work out of the box in Windows for earlier versions of Java. I think this is fixed as of Java 11 but you should check and let me know. -### The MIDI Interface -Edisyn makes up to three MIDI device connections. The *Receiving Device* is the MIDI device from which we will accept -patches. This is usually your synthesizer. The *Sending Device* is the MIDI device to which we will send -patches and parameter changes. We'll also need a channel for the Sending Device so we can send test notes. +### Installation and Running on Linux -Optionally you can route your controller keyboard through Edisyn to play the sounds directly if you wish. To do this, -the *Keyboard Device* is the MIDI Device of your controller keyboard. You'll also specify an incoming keyboard -channel of course. This can be set to "Any" for any channel (Omni). +I'm told that Edisyn works fine if you have installed *Java 8*. After this: -### Sending and Recieving Parameters +1. Download [Edisyn's jar file](https://github.com/eclab/edisyn/raw/master/jar/edisyn.jar) located in the "jar" directory. -If you change a widget in the editor, Edisyn will send the appropriate sysex command to the synthesizer to change it on -the synth as well. Additionally, if you change a parameter on the synthesizer and it forwards a *sysex* command to Edisyn, -then Edisyn will update the appropriate widget in the editor. At present Edisyn does't support CC commands from the -synthesizer (maybe later). So (for example) on the Blofeld you'll need to change the machine to *send sysex* -- not CC only -- when changing parameters on the synth. +2. You'll need to figure out how to make it so that double-clicking on the jar file launches it in java. In Ubuntu, here's what you do: right-click on the jar file icon and choose "Properties". Then select the "Open With" tab, and select your Java VM (for example "Open JDK Java 8 Runtime"). The press "Set as Default". This makes the Java VM the default application to launch jar files. -### Per-Synth Specific Notes and Bugs +3. Thereafter you should be able to just double-click on the file to launch Edisyn. -Gotchas and important things to know are contained in the About Tab in each synth editor window. You should read it before using the editor for that synth. -## Caveats +### Running from the command line (OS X, Windows, Linux) -To work around some bugs in OS X Midi and CoreMIDI4J, Edisyn's architecture at present does not let you -plug in new devices (or remove them) after Edisyn has been launched. If you need to do so, restart Edisyn -(for now). +1. Make sure Java is installed. -Randomize isn't very useful right now. I'm working on it. +2. Download [Edisyn's jar file](https://github.com/eclab/edisyn/raw/master/jar/edisyn.jar) located in the "jar" directory. -Everything has to be sent via sysex for the moment: I don't have code written to make it easy to send CC or NRPN -if you wanted to build a patch which did that. +3. Run Edisyn as: `java -jar edisyn.jar` -Popping up a new panel is slow. Profiling suggests that the primary reason for this is that JComboBox construction -is slow. So I can't get around it. diff --git a/docs/manual/0.png b/docs/manual/0.png new file mode 100644 index 00000000..ac3f8258 Binary files /dev/null and b/docs/manual/0.png differ diff --git a/docs/manual/1.png b/docs/manual/1.png new file mode 100644 index 00000000..87bfbb67 Binary files /dev/null and b/docs/manual/1.png differ diff --git a/docs/manual/2.png b/docs/manual/2.png new file mode 100644 index 00000000..c7993c37 Binary files /dev/null and b/docs/manual/2.png differ diff --git a/docs/manual/3.png b/docs/manual/3.png new file mode 100644 index 00000000..09437d78 Binary files /dev/null and b/docs/manual/3.png differ diff --git a/docs/manual/4.png b/docs/manual/4.png new file mode 100644 index 00000000..60d042f0 Binary files /dev/null and b/docs/manual/4.png differ diff --git a/docs/manual/5.png b/docs/manual/5.png new file mode 100644 index 00000000..fa411620 Binary files /dev/null and b/docs/manual/5.png differ diff --git a/docs/manual/6.png b/docs/manual/6.png new file mode 100644 index 00000000..021dc22c Binary files /dev/null and b/docs/manual/6.png differ diff --git a/docs/manual/7.png b/docs/manual/7.png new file mode 100644 index 00000000..36da167f Binary files /dev/null and b/docs/manual/7.png differ diff --git a/docs/manual/ConstrictorPanel.png b/docs/manual/ConstrictorPanel.png new file mode 100644 index 00000000..c6ffcb16 Binary files /dev/null and b/docs/manual/ConstrictorPanel.png differ diff --git a/docs/manual/Edisyn.pdf b/docs/manual/Edisyn.pdf new file mode 100644 index 00000000..55952f54 Binary files /dev/null and b/docs/manual/Edisyn.pdf differ diff --git a/docs/manual/Edisyn.tex b/docs/manual/Edisyn.tex new file mode 100644 index 00000000..7290d315 --- /dev/null +++ b/docs/manual/Edisyn.tex @@ -0,0 +1,1293 @@ +%%%% Copyright 2017 by Sean Luke +%%%% Distributed Under the Apache 2.0 License + + +\documentclass{article} +\usepackage{fullpage} +\usepackage{mathpazo} +\usepackage{microtype} +\usepackage{graphicx} +\usepackage{wrapfig} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{array} +\usepackage{color} +\usepackage{rotating} +\usepackage[nobottomtitles*]{titlesec} +\usepackage[noend]{algorithmic} +\usepackage{algorithm} + +\sloppy +\newcommand\ignore[1]{} + +\newcommand\myfrac[2]{#1/#2} +\begin{document} + +\noindent {\Huge\bf Edisyn}\\[0.5em] +{\large \bf A Java-based Synthesizer Patch Editor, Version 17\\[0.2em] +By Sean Luke\qquad sean@cs.gmu.edu}\\[0.2em] + +\vspace{-1em} +\setcounter{tocdepth}{1} +\tableofcontents + +\clearpage + +%\vspace{0.5em} +\section{About Edisyn} + +Edisyn is a no-nonsense synthesizer patch editor for the editing and parameter exploration of a variety of synthesizers. It is not skewmorphic or skinnable: its design is plain and consistent. Edisyn is free open source. Edisyn currently supports the following synthesizers: + +\begin{itemize} +%\item Futuresonus Parva +\item Dave Smith Instruments Prophet '08 (Single) +\item E-Mu Morpheus and Proteus (Single, Hyperpreset, and MidiMap) +\item Kawai K1, K1m, and K1r (Single and Multimode) +\item Kawai K4 and K4r (Single and Multimode) +\item Kawai K5 and K5m (Single) +\item Korg SG Rack (Single and Multimode) +\item Korg Microkorg +\item Korg Microsampler +\item Korg Wavestation SR (Performance, Patch, and Wave Sequence) +\item Oberheim Matrix 1000 (Single) +\item PreenFM2 (Single) +\item Waldorf Blofeld Desktop, Blofeld Desktop SL, and Blofeld Keyboard (Single and Multimode) +\item Waldorf Microwave II, Microwave XT, and Microwave XTk (Single and Multimode) +\item Yamaha DX7 Family (DX7, TX7, TX216/816, Dexed, etc.) (Single) +\item Yamaha TX81Z (Single and Multimode) +\end{itemize} + +You'll note a pattern among these synthesizers: many are very difficult to program.\footnote{So why are the Prophet '08, Blofeld, and Microwave XT there then? Because in fact these synthesizers are all ones I own or have had access to.} That's one of my interests. Edisyn's patch editors try to cover the most common needs, and so it does not support patches for global parameters, nor for wave, sample, or wavetable editing etc. Additionally, though it can do bulk downloads and save them as individual patches, Edisyn is {\bf not at present a librarian tool}. You should use a good free librarian software program, such as SysEx Librarian on the Mac. + + +\section{Starting Edisyn} +\label{startingedisyn} + +\begin{wrapfigure}{r}{2.2in} +\vspace{-2em}\includegraphics[scale=0.3]{0.png} +\vspace{-3em}\caption{Initial Synthesizer Dialog}\label{initialsynthpanel} +\vspace{-1em} +\end{wrapfigure} + +If you're on a Mac, Edisyn will look like a standard application, just double-click on it. On other platforms, Edisyn comes as a single Java jar file. Just double-click on the jar file (you'll have to have Java installed) and Edisyn should launch. + +You'll first be presented with the dialog at right, asking you to choose a synthesizer patch editor. You can either connect to a synth then and there, or run in {\bf Disconnected Mode}, where you're not attached to MIDI. You can also quit immediately. + +\begin{wrapfigure}{r}{2.7in} +\includegraphics[scale=0.3]{1.png} +\vspace{-1em}\caption{MIDI Dialog}\label{mididialog} +\vspace{-3em} +\end{wrapfigure} + +Edisyn will now build a patch editor for you and display it. But unless you chose {\bf Disconnected Mode}, it'll first ask you to set up MIDI for this editor. The dialog at right presents you with up to 6 fields (5 are shown here): + +\begin{itemize} +\item The USB MIDI Device from which you will {\bf Receive} MIDI data sent by the synthesizer. Here we are sending to a Tascam US-2x2 interface, which presents itself as a generic, nameless device. (BTW, if you're on a Mac and you don't like a generic name for your device, go to the Audio MIDI Setup application in the Utilities folder, double-click on the device, and change its name.) +\item The USB MIDI Device to which you will {\bf Send} MIDI to ultimately be sent to the synthesizer. Here again we are sending to the Tascam US-2x2 device. +\item The {\bf Channel} on which the synthesizer is listening. (Here, 1). +\end{itemize} + +\begin{itemize} +\item (Not visible here) The optional {\bf ID} of the synthesizer. Some synthesizers require a special ID embedded in their sysex so they can tell that the message is for them rather than another copy of the same synthesizer. (The Yamaha TX81Z doesn't have an ID, so it's not displayed in this example). +\item The USB MIDI Device from which you will receive MIDI data sent by a {\bf controller}. This may be a controller keyboard to play test notes on the synthesizer, or it may be a control surface to send CC data to the synthesizer or to Edisyn itself. Here we are receiving from an Arturia Beatstep. +\item The {\bf Channel} over which you will receive MIDI data sent by a {\bf controller}. This can be any specific MIDI channel, or (in this example) ``Any'', meaning any channel or OMNI. +\end{itemize} + +If you are not connected to MIDI, or if you cancel, then Edisyn will inform you that you must continue in {\bf Disconnected Mode}. + +\paragraph{Important Note} If your USB MIDI device is manually disconnected, Edisyn won't know until you ask Edisyn to send the synth something (perhaps changing a parameter or uploading a patch). At that point, Edisyn will get a clue and the patch editor window will change to {\bf Disconnected Mode}.' + + +\section{Edisyn Patch Editors} + +An Edisyn patch editor is a single window with multiple tabbed panes. You can switch tabs by clicking on them or via shortcuts (see the {\bf Tabs Menu}). The far-right tab is the {\bf About Tab}. It gives you information about the eccentricities of the synthesizer that require custom behavior in Edisyn (they all do!). You should read it carefully to understand how Edisyn will interact with your synthesizer. + +\paragraph{Categories} +At right is a typical tab pane. You'll note that various widgets are grouped together in regions (called {\bf Categories}). There are four categories shown here. Three are arbitrary categories for this synthesizer: ``Global'', ``LFO'', and ``Controllers''. They're in various colors to differentiate them. Other categories will be found in other tab panes. But one category is special: the {\bf Synthesizer Category}, always shown in white, here named ``Yamaha TX81Z''. It normally contains the patch name and bank/patch number. + +\begin{wrapfigure}{r}{3in} +\vspace{-2em}\includegraphics[scale=0.3]{2.png} +\vspace{-3em}\caption{Typical Patch Editor Panel (TX81Z)}\label{typicalpatcheditorpanel} +\vspace{-2em} +\end{wrapfigure} + + +\paragraph{Widgets} Edisyn has a number of widgets. Here are a few of them: + +\begin{itemize} + +\item The {\bf Patch Display}, currently showing patch ``I004''. Sometimes this display will be inaccurate, particularly if you manually change the patch on the synthesizer while Edisyn is running; or if Edisyn has no idea what the patch should be (it'll usually display a default value like, in this case, ``A001''. + +\item The {\bf Patch Name Button}, currently showing ``DEMO SOUND''. Click on this button to change the name of your patch. A dialog will pop up to let you change the sound, with an additional {\bf Rules} button to explain the constraints the synthesizer places on patch names. + +\item Displays of {\bf Keyboards}. Select a key! +\end{itemize} + +\begin{itemize} +\item Various {\bf Dials}. These are semicircles in gray, partly in some other color, with a value in the center. Dials vary in orientation. Most look sort of like a ``C'', with the zero point at the bottom center. Other dials are symmetric, such as the ``Breath Ctrl. Pitch Bias'' dial (bottom row, second from right in the Figure), and have zero point at center top. Occasionally dials have other orientations: the goal is to keep the zero point centered (at top or bottom). + +You change values in a Dial by clicking on the dial and dragging vertically. You can also double-click on a dial to reset it to a default value (often zero). If the Dial doesn't have the finesse you require to hit an exact value, hold down the {\bf Alt} (or on the Mac, the {\bf Option} key while dragging and you'll get 4\(\times\) the resolution. Hold down the {\bf Control} key and you'll get 16\(\times\) the resolution. Hold down both keys and you'll get 64\(\times\) the resolution! Finally, you can two-finger drag (on the Mac), or spin the mouse wheel to move the value by exactly 1 unit. + + +\item Some {\bf Checkboxes} (such as ``Portamento'') and {\bf Pop-Up Choosers} or ComboBoxes (such as ``Wave'', set to ``Sawtooth''). These are should be straightforward. + +\item Various {\bf Pictorial Displays}. Here, changing the ``Algorithm'' dial will modify the Algorithm Display immediately to the right of it. + +\item Various {\bf Envelope Displays}. Edisyn can draw envelopes using a variety of procedures. Consider the Waldorf Microwave envelopes in Figure \ref{envelopedisplays} above, for example. The first two envelopes are ADSR envelopes, but the third is the Microwave's famous ``Wave Envelope'', an eight-stage envelope with two different looping intervals (shown below it), and with two special end times marked with vertical lines (here, the dashed line is where optional sustain occurs, and the solid line is the end of the wave). The last envelope is the Microwave's ``Free Envelope'', a four-stage envelope unusual in that it can have both positive and negative values: the dashed line is the axis. + +Mose Edisyn envelope displays are read-only -- you can't draw the dots. But that's not always the case: for example the Kawai K5 harmonics display can be extensively edited by mouse. + +\item {\bf Action Buttons}. Some patch editors have buttons on them which perform actions rather than edit or display values. For example many multimode-patch editors have buttons that pop up single patch editors for the various individual patches. + +\end{itemize} + +If you are connected to a synthesizer over MIDI, then changing a widget will modify the underlying patch parameter in real time, if the synthesizer supports this. Also, if you modify a parameter on the synthesizer, then Edisyn will update the corresponding widget or widgets (again, if the synthesizer supports this). + +\begin{figure}[t] +\begin{center} +\includegraphics[scale=0.6]{4.png} +\end{center} +\caption{Envelope Displays of the Waldorf Microwave II, XT, and XTk.} +\label{envelopedisplays} +\end{figure} + +\section{Creating and Setting Up Additional Patch Editors} + +A patch editor is created by selecting one of the various {\bf New...} menu options in the {\bf File} menu. You have to create a new a patch editor before you can start loading a patch from a file or from the synthesizer. You can also {\bf Duplicate} an existing patch editor (in the {\bf File} menu). This will exactly duplicate the existing patch as well. + +Whenever you create a new patch editor or duplicate one, you will once again be asked to set up MIDI as discussed in Section \ref{startingedisyn}, or to run in Disconnected Mode. + +\subsection{Persistence} + +Now would be a good time to mention an Edisyn feature you may never notice otherwise: many things are {\bf persistent}. For example, if you choose ``Arturia Beatstep'' as the controller for your Blofeld patch, the next time you call up a Blofeld patch editor, ``Arturia Beatstep'' will be presented as the default choice in the MIDI Devices window, assuming your Arturia Beatstep is plugged in. This goes for everything in the MIDI Devices window. Furthermore, if you pop a new patch editor for a synthesizer you have never edited before, the Arturia Beatstep will be the default option for that one too (until you change it one time). And these options are per synthesizer type. + +Persistence appears in other places too. For example, the Initial Synthesizer dialog will default to the last synth you chose in that dialog. And certain many choices are persistent as well. + +\section{Loading and Saving Files} + +Edisyn is capable of reading both sysex files or MIDI files and extracting sysex patch data from them. It can in some situations read files which contain many patches: but Edisyn only writes files with a single patch per file, and only in sysex file format. + +You can save your edited patch via the {\bf Save} and {\bf Save As...} options in the {\bf File} menu, and you can load a patch via the {\bf Load...} option. This is called {\it Load} and not {\it Open} because you can only load a file into an existing patch editor: you cannot create a new patch editor automatically on opening a file. If the sysex file is not for your patch editor, but Edisyn still recognizes its data, it'll ask if you want to load for a different synthesizer. If Edisyn doesn't recognize the data at all, it'll tell you it's best guess as to the manufacturer of the device which produced it.\footnote{Thanks to the MIDI Association for updating their database to make this possible in Edisyn.} + +Most patch editor files are sysex dumps ending in the extension {\tt .syx}. These files are usually exactly the same sysex data that you'd normally dump to your synthesizer using a patch librarian software program. There are exceptions however. For example, some synthesizers, like the PreenFM2, have no sysex to speak of at all: they exchange parameters entirely over NRPN. In this situation, Edisyn has invented a sysex file just for the PreenFM2. It obviously won't work in your librarian software. + +Some patch editor files have encoded the sysex dumps as MIDI files (typically ending with the extension {\tt .mid}. Edisyn can extract sysex from these files as well. + +\paragraph{Loading a Bulk-Sysex or Bank-Sysex File} +Edisyn tends to work with files which contain a single patch each. However many patch files on the internet are what I call {\it bulk sysex} or {\it bank sysex} files. A bulk sysex file contains a bunch of individual patch sysex messages concatenated together\,---\,you could just cut it up into separate single-patch files. Bulk sysex files are common to many synthesizers, such as the DSI Prophet '08. If Edisyn loads a bulk sysex file, it will present you with the option to select and edit a single patch from that file. + +It is possible that a bulk sysex file contains patch entries for different kinds of editors. For example, a bulk sysex file might contain entries for both single patches and multi patches, and these are loaded into different editors. Edisyn will inform you that there are patches for different editors involved, let you select which editor you're interested in, and then let you select which patch you want to edit. + +Some synthesizers have special {\it bank sysex} messages which contain an entire bank (or in some cases, an entire synthesizer's memory worth) of patches. These messages could be stored in files or arrive from the synthesizer itself. Only a few Edisyn patch editors know how to deal with these messages: notably the DX7 patch editor can handle these just fine. In this case, you will be given three options, to upload the entire bank to the synthesizer, to save the entire bank to a file, or to select a single patch from the bank and edit it. + +It's theoretically possible that a bulk sysex file might contain multiple bank sysex messages. Do not expect Edisyn to handle these properly, though you might get lucky. + +Edisyn cannot construct (and thus save or upload) new bank sysex messages or files. + +\paragraph{Batch Downloads} +Edisyn also has limited support for batch-downloading patches, one by one, and saving them to your disk as separate files. To do this, choose {\bf Batch Download...} from the {\bf File} menu. You'll be asked to specify the directory in which to save patches, and also first patch and the final patch, and then downloading will commence. Note that if your final patch is ``before'' the first patch, then Edisyn will wrap all the away around to get to the final patch. For example, if your synth has ten patches 1....10, and you choose 8 as your first patch and 2 as your last patch, then Edisyn will download in this order: 8, 9, 10, 1, 2. + +If Edisyn can't download a particular patch (the synth isn't responding), it'll try again and again until successful. So if it gets stuck, +you can always stop batch-downloading at any time by choosing {\bf Stop Downloading Batch} from the {\bf File} menu. Note that you can still screw with knobs, etc. while Edisyn is busy downloading batches: but don't do that. You're just messing up the batches getting saved.\footnote{I may change this in the future to something less fragile.} + +Also note that as a failsafe Edisyn only allows the frontmost window to receive data over MIDI. This means that while you're batch-downloading, you can't go to some other patch editor: the downloading patch editor must stay in front. You can go to another application though (read a web browser say). + +\paragraph{Exporting to Text} +Perhaps you might wish to describe your patch on you blog or your favorite forum. if you choose {\bf Export to Text...} from the {\bf File} menu, Edisyn will write out all of its patch parameters to a text file. Edisyn may occasionally break out parameters more than your synthesizer does: though usually it's pretty close to a one-to-one mapping. The parameter names can be cryptic sometimes: Edisyn often (not always) names parameters in a manner fairly similar to how they're specified by the synthesizer manufacturer in its MIDI Sysex document, and synth manufacturers are not known for being consistent in their naming between the sysex document and the user manual. + + +\section{Communicating with a Synthesizer} + +First things first: if you're working in Disconnected mode, you'll need to set up MIDI before you can communicate with your synthesizer. This is done by selecting {\bf Change MIDI} in the {\bf MIDI} menu. (By the way, you can go Disconnected by selecting {\bf Disconnect MIDI} in the {\bf MIDI} menu as well). Remember that you have to connect USB devices to your computer {\it before} starting up Edisyn, or it won't see them, due to a bug in the MIDI subsystem. + +Now that you're up and running, if you change widgets in the patch editor, many (not all) synthesizers will automatically update themselves. The opposite happens as well: changing a parameter on the synthesizer will update it in Edisyn. See the About pane to determine if your synthesizer can't do this. + +By selecting {\bf Request Current Patch}, you can also ask your synthesizer to send you a dump of whatever patch it is currently running. It is often the case that synthesizers respond in such a way that Edisyn cannot tell what the patch number or bank is. In these cases Edisyn will reset the patch number to some default (like A001). + +{\bf Request Patch...} will ask the synthesizer to send Edisyn a specific patch that you specify. Edisyn often (not always) does this by first asking the synthesizer to change to that patch and bank, and then requesting the current patch. + +{\bf Send to Current Patch} will dump Edisyn's current patch to the synthesizer, instructing it to only update its local working memory, and not to store the patch in permanent memory. This operation is primarily used to sync up certain synthesizers which do not update themselves in real-time in response to parameter changes you make. + +{\bf Send to Patch...} will ask the synthesizer to change to a new patch and bank which you specify, then dump Edisyn's current patch to the synthesizer in its working (not permanent) memory. This also isn't used all that much: but some synthesizers (like the PreenFM2 or TX81Z) cannot be permanently written to remotely. Instead you send to a patch, then store the patch manually on the synthesizer itself. + +{\bf Sends Real Time Changes} controls whether the Edisyn will send parameter changes to the synthesizer in real time in response to you changing widgets in the patch editor. This isn't necessarily determined by the synth model. For example, the default ROM for the Oberheim Matrix 1000 cannot handle real-time changes: but ROM versions 1.16 or 1.20 (later bug fixes by the Oberheim user community) allow real-time changes with no issue. + +{\bf Write to Patch...} will ask the synthesizer to change to a new patch and bank which you specify, then dump Edisyn's current patch to the synthesizer to its permanent memory. + +\vspace{1em} + +Note that various synthesizers cannot do one or another of these tasks. When this happens, that feature will generally be disabled in the menu. As always, read the About Tab to learn more about what's going on with that synthesizer model. See Section \ref{inconsistent} for some information and griping about all this. + +A synthesizer can also offer its own sysex messages to Edisyn without Edisyn requesting them. Edisyn will try to handle these appropriately. Some synthesizers might send special {\it bank sysex} messages which contain an entire bank (or in some cases, an entire synthesizer's memory worth) of patches. Only a few Edisyn patch editors know how to deal with these messages: notably the DX7 patch editor can handle these just fine. In this case, you will be given three options, to upload the entire bank to the synthesizer (again), to save the entire bank to a file, or to select a single patch from the bank and edit it. + + +\subsection{Playing Test Notes} + +If you don't have a controller keyboard, you can send a test note to your synthesizer by choosing {\bf Send Test Note}. You can also toggle whether Edisyn constantly sends a stream of test notes by choosing {\bf Send Test Notes}. And you can shut off all sound on the synthesizer with {\bf Send All Sounds Off} (this also turns off sending test notes).\footnote{{\bf Send All Sounds Off} does three things in a row. First it sends an ``All Sounds Off'' message to all channels. Then it sends an ``All Notes Off'' message to all channels (because some synthesizers respond to All Sounds Off but not All Notes Off, or vice versa). Finally, it does a simple Note Off for any note it may have been playing, to all channels, because there exist a few synths that respond to {\it neither} All Sounds Off nor All Notes Off.} + +Edisyn gives you various options for adjusting the test note you send (though it's always a ``C''). You can change the length of the test notes you send in the {\bf Test Note Length} submenu. You can change the pitch with {\bf Test Note Pitch}. And you can change the volume with {\bf Test Note Volume}. + +Setting the {\bf Pause Between Test Notes} will change how long Edisyn waits, beyond the note length itself, before it plays the next note if you have {\bf Send Test Notes} on. It doesn't affect how fast you can play test notes on your own. One special setting is {\bf Default}: this is defined as an additional pause equal to the note length if the note length is less than 1/2 seconds; or a pause of 1/2 second if the note length is greater than this. + +Some synthesizers (such as the Yamaha DX) feature notoriously long release times on their envelopes, so if you're doing hill-climbing (see Section \ref{hillclimb}) or otherwise repeatedly sending test notes, the notes may bleed into each other such that you can't hear the note clearly. To fix this, you can set {\bf Send All Sounds Off Before Note On} to true. This will cause the Send Test Note facility to abruptly shut off all sound, like {\bf Send All Sounds Off} does, just before sending a new note. + +\subsection{Testing the Incoming Connection} + +If you're not sure if you have MIDI data coming to Edisyn from your synthesizer, select {\bf Report Next Synth MIDI} from the {\bf MIDI} menu. Then have your synth send any kind of MIDI message to Edisyn\,---\,a note, a sysex message, whatever. For example, you could have Edisyn request a sysex dump from the synth. At any rate, if Edisyn pops up a window telling you the message, then you have a live connection. + +\section{Communicating with a Controller} + +The MIDI Dialog (Section \ref{startingedisyn}) also lets you choose a device and MIDI channel for incoming messages from a control surface or controller keyboard. Using this keyboard you can: + +\begin{itemize} +\item Play the synthesizer (through Edisyn). +\item Control the synthesizer (CC and Program Change messages, etc.) +\item Control widgets in Edisyn +\end{itemize} + +\subsection{Testing the Incoming Connection} + +If you're not sure if you have MIDI data coming to Edisyn from your controller, select {\bf Report Next Controller MIDI} from the {\bf MIDI} menu. Then have your controller send any kind of MIDI message to Edisyn\,---\,for example, play a note. If Edisyn pops up a window telling you the message, then you have a live connection. + +\subsection{Remote Control of your Synthesizer} +If you play a note, do a pitch bend, etc., on your control surface, and {\bf Pass Through Controller Data} is set, then Edisyn will route all of those MIDI messages directly to your synthesizer (changing the messages' channel to the one that Edisyn is using to talk to the synthesizer). Control Change (CC) and NRPN messages from your control surface are passed through only if you have {\it also} toggled {\bf Pass Through All CCs} in the {\bf Map} menu. Otherwise they {\it might} used to control Edisyn' via its parameter mapping (see the end of Section~\ref{remotecontrolofedisyn}). + +If your controller is sending these messages on Edisyn's Controller Channel, Edisyn usually just routes them through unchanged, but it {\it might} route those messages to some other channel instead. This only happens in certain patch editors where it's appropriate. For example, the Kawai K4/Kr4 [Drum] Patch Editor needs to forward note messages like these to the Kawai K4's ``Drum'' channel to hear them. The ``Drum'' channel is different from the Kawai's primary MIDI communication channel (which is what Edisyn's Send Channel is set to). + +\subsection{Remote Control of Edisyn} +\label{remotecontrolofedisyn} + +Edisyn is capable of {\it mapping} Control Change (CC) messages or NRPN messages from your control surface to parameters in your patch editor. Each patch editor type can learn its own set of CC and NRPN mappings. + +\paragraph{Mapping a Parameter} + +Mapping a parameter is easy: + +\begin{enumerate} +\item Choose one of three MIDI mapping menu options discussed next. The title bar will say ``LEARNING''. +\item Select the widget you want to map, and modify it slightly. The title bar will change to ``LEARNING {\it parameter}[{\it range}]'', where {\it parameter} is Edisyn's name for the synthesizer parameter in question, and its values are in \((0...\text{\it range}-1)\). The title bar might also tell you what the {\it previous} mapping was. If \(\text{\it range} > 127\) then you should think about mapping with 14-bit NRPN instead of CC. +\item Press or spin the knob/button on your controller. You're now mapped! +\item If you have chosen an absolute mapping, you'll want to change your controller's range to \((0...\text{\it range}-1)\). +\end{enumerate} + +\noindent Edisyn accepts any of the following MIDI Control commands. + +\begin{itemize} +\item {\bf Absolute CC}\quad The value of the CC sent is exactly what the parameter will be set to (between 0...127). To map, choose {\bf Map CC/NRPN} in the {\bf Map} menu. This style is particularly useful for potentiometers or sliders. You are not permitted to map CC numbers 6, 38, 98, 99, 100, or 101, or Edisyn will think you're sending NRPN. So you only have 121 CCs to play with. +\item {\bf Relative CC}\quad Here, the CC value you send indicates how much to {\it add to} or {\it subtract from} the existing parameter value.\footnote{Specifically, a value \(x=64\) means 0 (add nothing), a value \(x<64\) means to subtract \(64-x\) from the current value, and a value \(x>64\) means to add \(x - 64\) to the current value.} This style is supported by a number of controllers and is useful for encoders.\footnote{You could also map a pair of pushbuttons to be up/down cursors using this method: set up the ``down'' pushbutton to send 63 and the ``up'' pushbutton to send 65.} For example, the Novation controller series calls this ``REL1'' or ``REL2''\footnote{The difference being that in REL2 mode, if you spin the encoder rapidly, the amount added/subtracted is nonlinearly more than expected, whereas in REL1 the speed doesn't matter, all that matters is how far the encoder was turned. Novation controllers also have a relative CC mode called ``APOT'', which is not supported.}, and the BeatStep calls this ``Relative 1''.\footnote{The Beatstep also has relative CC modes called ``Relative 2'' and ``Relative 3'', which are not supported.} To map, choose {\bf Map Relative CC} in the {\bf Map} menu. Again, you're not permitted to map CC numbers 6, 38, 98, 99, 100, or 101. +%\item {\bf Relative CC ``0''}\quad Here, the CC value you send again indicates how much to {\it add to} or {\it subtract from} the existing parameter value. In this form of Relative ŒCC, 0 means 0 (add nothing), a high value \(64x\geq64\) means to add \(x\) to the current value. This style is also supported by a number of controllers and is useful for encoders. To map, choose {\bf Map Relative CC[0]} in the {\bf Map} menu. +\item {\bf NRPN}\quad You are permitted to map any NRPN parameter you like. The value of the CC sent is exactly what the parameter will be set to: all 14 bits. If your controller can only send 7-bit NRPN, then you should configure it to send ``Fine'' or ``LSB-only''. Edisyn also supports the NRPN Increment and Decrement options, though those are rarely supported by hardware. To map, choose {\bf Map CC/NRPN} in the {\bf Map} menu. +\end{itemize} + +\paragraph{Mapping by Panel or by MIDI Channel} + +By default, Edisyn only maps and responds to CCs (or NRPN etc.) if they are on Edisyn's controller channel. Each tab in a patch editor can have its own unique set of mappings: for example, the Oscillators tab might use CC\#1 to change the Start Wave parameter, but the Envelopes tab might use CC\#1 to change the attack of Envelope 1. The mapping being used at the moment depends on which tab is being displayed. + +Alternatively, if you toggle {\bf Do Per-Channel CCs}, you can ask Edisyn to instead remember the channel of mapped CCs (or NRPN). Then you can map CC\#1, on (say) channel 4, to the Start Wave parameter on the Oscillators tab, and map CC\#1 on channel 7 to the attack of Envelope 1 on the Envelopes tab. If CC\#1 arrives on channel 4, the Start Wave parameter will be adjusted even if the Oscillators tab isn't being shown; similarly if CC\#1 arrives on channel 7, then the attack of Envelope 1 will be adjusted even if the Envelopes tab isn't being shown. + +If your controller can only send a few CCs (it only has a few knobs and buttons) I would use the first option (per-panel mapping). If your controller can send a vast number of CCs, or you're comfortable with it from experience with your DAW, you might use the second option. + +\paragraph{Where Data Goes} + +Whether Edisyn will pass through data to your synth, or block it, or intercept it in order to map it, is as follows. If the data is CC/NRPN, then Edisyn must decide whether to {\it intercept and map} it. If you have selected {\bf Pass Through All CCs}, Edisyn isn't permitted to intercept any CC/NRPN data at all. Otherwise Edisyn will intercept the CC/NRPN data if it is on your Controller Channel, or if the Controller Channel is OMNI, or if you have selected {\bf Do Per-Channel CCs}. + +If Edisyn isn't intercepting and mapping the data, or the data is something other than CC/NRPN, then Edisyn must then decide whether to {\it block} the data or {\it pass it through} to your synth. This is easy: the data is passed through only if {\bf Pass Through Controller MIDI} is selected. + +There are many situations where these combinations are useful. Here's a fun example. Suppose your synth doesn't respond to CC (only NRPN or Sysex) but you'd like to control it from your DAW, which {\it only} does CC, as is the case for many bad DAWs. You could set up the DAW as your Edisyn controller and map CCs to various synth parameters. Then you'd pass through non-CC data via Edisyn to the synth, but intercept CC data from the DAW to update parameters via Edisyn. + +\section{Communicating with a Software Synth or Digital Audio Workstation} + +In some situations you might wish to get Edisyn to communicate with a software synth: for example Dexed\footnote{https:/\!/asb2m10.github.io/dexed/} is a nice DX7 emulator and works well with Edisyn. Or perhaps you might want to use Edisyn to translate CC messages from your DAW into Edisyn parameter changes, which then get forwarded to a synthesizer as sysex (see Section \ref{remotecontrolofedisyn} to learn how to map CC messages to parameter changes). + +\begin{wrapfigure}{r}{3in} +\begin{center}\vspace{-1em}\includegraphics[width=3in]{Loopback.pdf}\end{center} +\vspace{-2em} +\caption{A MIDI loopback connecting Edisyn with a software synthesizer via a virtual device.}\label{loopback} +\end{wrapfigure} + +You'd think it'd be easy to connect directly to another piece of software on your computer. But you'd be wrong! The problem is that Edisyn, because it's written in Java, can only connect to {\it MIDI devices}, and your software synthesizer or DAW has probably not registered itself as a device\,---\,it likewise probably is designed only to connect to MIDI devices. + +To get around this, you need to make a {\it MIDI loopback}. This is where you create two {\it virtual devices} which are connected to one another. Edisyn and your software synth can see these devices. Consider Figure \ref{loopback}. If Edisyn outputs to Virtual Device X (say) and your software synth is set up to {\it input} from Virtual Device X, then it will receive what Edisyn outputs. + +Similarly, if you need your software synth to respond to Edisyn, you need to make a {\it second} loopback and hook it up in the reverse order. + +\paragraph{Making a Loopback Device} This varies depending on your operating system. + +\begin{itemize} +\item {\bf On the Mac}\quad First, open the application \textsf{/Applications/Utilities/Audio MIDI Setup}. Next, click on the ``IAC Driver'' icon to open the ``IAC Driver Properties'' window. Add a new port, named whatever you like. Check the box ``Device is Online''. This new port will be appear to Edisyn and to your software synth as the loopback device. You can add more ports to create more loopbacks. A loopback is only one-way: if you want Edisyn to send to {\it and} receive from a software synthesizer, you'll need to make two ports. + +\item {\bf On Windows}\quad There is no way to do this in Windows directly: instead you'll need to run a program which provides this service. Programs include {\sf loopMIDI}, {\sf loopBe1}, MIDIOx's {\sf MidiYoke}, and so on. Googling for ``loopback MIDI Windows'' will get you there. + +\item {\bf On Linux}\quad In most flavors of Linux, to get virtual devices running you'll first need to type the command \hbox{\tt sudo modprobe snd-virmidi} and then type in your password. \quad If you're using something like Gentoo or any other distro that does not come with this kernel module, you'll need to custom compile your kernel to get it. + +This procedure will create a bunch of of virtual devices with names like {\tt VirMIDI [hw:2,0,0]} or {\tt VirMIDI [hw:2,1,8]}. Select a device whose third number is 0 (such as {\tt VirMIDI [hw:2,0,0]} or {\tt VirMIDI [hw:3,1,0]}, but not {\tt VirMIDI [hw:2,1,1]}). Have Edisyn send to this device and have the software synthesizer listen from the same device. If you want to hook Edisyn and your synth up the other direction (so Edisyn receives from the synthesizer), you'll need to select a second virtual device. +\end{itemize} + +\section{Editing and Exploratory Patch Creation} + +Edisyn has a number of facilities to help you program your synthesizer, including tools to help you wander through the possible space of patches to hunt for the sound you want. Here's what you can do: + +\paragraph{Undo and Redo} Edisyn has infinite levels of undo and redo. When you change a parameter or do a wholesale modification, this can be undone, as can patch dumps and merges from the synthesizer. Individual parameter changes made manually on the synthesizer are not undoable even if they're reflected in Edisyn (it'd be too many). Loading and saving patches is not undoable. See the {\bf Edit} menu. + +\paragraph{Reset} You can reset the patch editor to its ``init patch''. Just choose {\bf Reset} in the {\bf Edit} menu. + +\paragraph{Category Cut/Paste, Distribution, and Reset} Each category has a pop-up menu you get when you right-click or shift-click (or two-finger click on a Mac trackpad) on the category name. You can: + +\begin{itemize} +\item {\bf Copy Category}\quad Marks the category to be pasted into other compatible categories. +\item {\bf Paste Category}\quad Copies over all the parameters from the ``copy'' category, if it is compatible. +\item {\bf Distribute}\quad Copies the last-modified parameter to all similar parameters in the category. For example, if you modified a step sequencer step, this might copy its value to all 15 other steps. {\bf Note} that you won't be able to select this option until you have actually {\it modified}, even slightly, some parameter inside the Category\,---\,perhaps a dial, say. +\item You can also restrict your {\bf Copy, Paste,} or {\bf Distribute} to mutation parameters only. +\item {\bf Reset} \quad This resets all the parameters in the category to their defaults. +\end{itemize} + +\paragraph{Tab Cut/Paste and Reset} For some synths, you can also cut/paste entire tabs. Choose these menu options under the {\bf Edit} menu: + +\begin{itemize} +\item {\bf Copy Tab}\quad Marks the tab to be pasted into other compatible categories. +\item {\bf Paste Tab}\quad Copies over all the parameters from the ``copy'' tab, if it is compatible. +\item {\bf Copy Tab (Mutation Parameters only)}\quad Marks the tab to be pasted into other compatible tabs, but only from the mutation parameters you have set. +\item {\bf Paste Tab (Mutation Parameters only)}\quad Copies over all the parameters from the ``copy'' tab, but only to the mutation parameters you have set. +\item {\bf Reset} \quad This resets all the parameters in the tab to their defaults. +\end{itemize} + +\paragraph{Randomize (by some amount)} You can add some randomness your patch parameters. Try a small value: values \(\geq 50\%\) are essentially full randomization. See the {\bf Randomize} submenu in the {\bf Edit} menu. Because it's so common to randomize, then undo and try again, you can also do undo-and-randomize-again as a single task: select {\bf Undo and Randomize Again} in the {\bf Randomize} submenu of the {\bf MIDI} menu. See below for a discussion of how randomization (called {\bf mutation}) works in Edisyn. + +\paragraph{Nudge} The nudge facility lets you push your patch to sound more and more like one of four other target patches you have chosen. You can use this, plus randomize, to wander about in the patch space. Before you can nudge, you have to first select patches to nudge towards. You can pick up to four patches by first setting up or loading the patch in your patch editor, then selecting one of {\bf Set 1} ... {\bf Set 4} in the {\bf Nudge} submenu of the {\bf Edit} menu. You don't have to ultimately select all four. + +Above the {\bf Set} options are four {\bf Towards} and four {\bf Away From} options, also in the {\bf Nudge} submenu of the {\bf MIDI} menu. When you set a patch, its current name will appear in the equivalent Towards/Away From option. The patch name is just a helpful reminder\,---\,it's entirely possible for four completely different patches to have the same name. + +When you chose any of {\bf Towards 1:...} through {\bf Towards 4:...}, your current patch will get {\bf recombined} with the target patch, by default by 25\%, to move it towards that target. Similarly, when you chose any of {\bf Away From 1:...} through {\bf Away From 4:...}, your current patch will get adjusted (through a form of recombination) to {\it move away from} the target patch, by default by 25\%. You can change the degree of recombination under the {\bf Set Nudge Recombination} menu. Additionally you can add some automatic mutation whenever you nudge: just set its amount under the {\bf Set Nudge Mutation} menu (by default it's 0\%). If you did a nudge and didn't like it, you can try a slightly different one with {\bf Undo and Nudge Again}. + +A hint. It's a good idea to select target patches which don't have some radical difference creating a nonlinearity in the space between them: for example, if you were doing FM, I'd pick patches which all used the same operator Algorithm. See below for a discussion of how recombination works in Edisyn. + +\paragraph{Merge (by some amount)} Merging is a lot like nudging. But instead of nudging towards a predefined target patch, you are asking your synthesizer to load a given patch, which Edisyn will then directly {\bf recombine} with your current patch to form a randomly merged patch. You specify the degree as a percentage: see the options in the {\bf Request Merge} submenu of the {\bf MIDI} menu. + +Some patch editors may not be able to perform merges because the synthesizers can't load specific patches: if your synth can't do {\bf Request Patch...}, it probably can't do a merge either. + +\paragraph{Load and Merge} This option, in the {\bf File} menu, allows you to load a file and merge it with your current patch in more or less the same way that {\bf Merge} works. The merge percentage is always 100\% (that is, half-half). + +\paragraph{Hill-Climb} Hill-Climbing repeatedly presents you with sixteen sounds and asks you to choose your top three preferred ones. Once you have selected the three best, it performs various recombinations and mutations on those sounds to prefer sixteen new ones and the process repeats again. The idea is for your preferences to guide the hill-climber as it wanders through the space of synth parameters until it lands on something you really like.\footnote{If you're technically inclined, this is basically an evolution strategy (ES) with an elitism of 1 and a biased mutation procedure. If you'd like to learn more about evolutionary computation methods, google for the free online book {\it Essentials of Metaheuristics} by me.} For more on the Hill-Climber, see Section \ref{hillclimb}. + +\subsection{Restricting Mutation and Recombination to Only Certain Parameters} +\label{restriction} + +You can restrict Mutation (Randomize) and Recombination (Nudge and Merge) to only affect a subset of parameters. To do this, choose {\bf Edit Mutation Parameters} in the {\bf Edit} menu. This will turn on {\it Mutation Parameters} mode (you'll see it in the window's title bar). You'll note that various widgets have now been surrounded with red frames. These widgets control synth parameters which are presently are being updated when you mutate a patch. + +\begin{wrapfigure}{r}{3in} +\vspace{-1em}\includegraphics[scale=0.3]{5.png} +\vspace{-3em}\caption{Editing Mutation Parameters}\label{mutationparameters} +\end{wrapfigure} + +You can change these of course: just click on them and you can remove them from being updated (or add them back).\footnote{Note that due to an error in Java's design, you can't click directly on a Combo Box (a pop-up menu) such as the ``Wave'' combo box in Figure \ref{mutationparameters}. But you can click on its title (the text ``Wave'').} You can also turn on (or turn off) all of the parameters in a category by double-clicking on the category title. The categories in Figure \ref{mutationparameters} are {\it Yamaha TX81Z}, {\it Global}, {\it LFO}, and {\it Controllers}. Finally, you can turn on all the parameters in the entire patch editor by selecting {\bf Set All Mutation Parameters} in the {\bf Edit} menu, or conversely turn them all of by selecting {\bf Clear All Mutation Parameters}. + +Some parameters, such as the patch name or certain other parameters, can't be mutated no matter what: these have been declared {\it immutable} by the patch editor. They will never have a red frame no matter how much you click them. + +The parameters you have selected will be the only ones changed when you mutate (randomize) a patch. But if you turn on {\bf Use Parameters for Nudge/Merge} in the {\bf Edit} menu, then recombination (nudging, merging) will be restricted to these parameters as well. + +Once you're done choosing parameters to mutate or recombine, just select {\bf Stop Editing Mutation Parameters} in the {\bf Edit} menu. + +Note that your parameter choices, as well as using them for recombination, are persistent: they're saved in the preferences. + +\subsection{Hill-Climbing} +\label{hillclimb} + +\begin{wrapfigure}{r}{3.6in} +\vspace{-2em}\includegraphics[scale=0.27]{HillClimbPanel.png} +\vspace{-2em} +\caption{The Hill-Climber Panel} +\label{hcpanel} +\end{wrapfigure} + +Edisyn's {\it hill-climber} is another of its patch-exploration tools. You select it by choosing {\bf Hill-Climb} in the {\bf Edit} menu. This will add a new tab to your patch editor labelled {\bf Hill-Climb}. When you fire up the Hill-Climber, it appears as an additional tab after your {\bf About} tab. Whenever you select a tab other than the hill-climber tab, the hill-climber will pause; when you go back to the hill-climber tab it will resume. You get rid of the hill-climber by selecting {\bf Stop Hill-Climbing} in the {\bf Edit} menu. + +In the Hill-Climber tab, make sure that the ``Method'' combobox is set to {\it Hill-Climb}: for other methods, see the Constrictor Section (\ref{constrict}). + +Edisyn's Hill-Climber repeatedly offers you sixteen new versions of patches and asks you to choose your top three preferences. After you have chosen them, the Hill-Climber will try to build a new set of sounds whose parameters are similar to your choices in various ways. The Hill-Climber builds new sounds by recombining your top three sound choices in certain ways and adding some degree of noise (mutation) to them. If you're lucky, Edisyn will head towards regions of the synthesizer's parameter space which make sounds you like. {\bf Hint: You will have more success with the hill-climber if you restrict the parameters being mutated to just those you want to explore.} Different kinds of synthesizers will also benefit from different amounts of exploration (mutation and recombination) rates. You'll need to tweak those as necessary. + +\paragraph{Candidates} This region holds the current candidate patches. When you start up the Hill-Climber, it will begin playing each candidate patch in turn. If you don't want Edisyn to play automatically, you can turn it off by choosing {\bf Send Test Notes...} in the {\bf MIDI} menu. Either way, you can manually play a patch by pressing its {\bf Play} button, or by typing the key located on that button. You can of course also choose which three patches you like the most (in order). + +\paragraph{Iterations} After you have selected your preferred patches, you can build a new set of patches from them by pressing the {\bf Climb} button. This will also increment the iteration number. If you don't like the patches that were built, you can try again by pressing the {\bf Retry} button. If you'd like to back up to a previous iteration, press {\bf Back Up}. Finally, to reset back to the very first iteration, press {\bf Reset} and choose ``From Original Patch'. + +The amount of mutation noise used when generating new patches is specified by the {\bf Mutation Rate} dial. Typically you'd select something around 5--10. Additionally, by default the Hill-Climber lets you select from 16 candidates. But if you press {\bf Big}, this set will expand to 32 candidates. + +\paragraph{Archive} If you like a patch and you'd like to hold onto it even after hill-climbing to the next iteration, you can place it in the {\it archive}. The archive consists of six patch locations: to copy a patch to a given archive location, click on its {\bf Options} button and select one of {\bf Archive to q} through {\bf Archive to v} (the Archive patches are so named because presently played by pressing keys q through v). Archived patches can also be selected to participate in hill-climbing: just pick a number under a given patch. + +\paragraph{Current and None} The {\it Current} category holds the patch currently being edited in your editor, and it too can be selected to participate in hill-climbing at any time. Finally, if you select a number in the {\it None} category (number 2, say) then no patch is selected for number 2 at this time. + +\paragraph{Additional Options} The {\bf Options} button holds some additional features besides just copying to the archive. + +\begin{itemize} +\item {\bf Keep Patch}\quad Loads the patch into the current patch in your patch editor. +\item {\bf Edit Patch}\quad Creates a new patch editor and loads the patch into that. Note that if you edit the patch in its editor, the {\it changes you make will be automatically reflected here}. +\item {\bf Save to File}\quad Saves the patch to a file. +\item {\bf Load from File}\quad Changes the patch to one loaded from a file. +\item {\bf Nudge Candidates to Me}\quad Nudges all the candidates towards the given patch. +\end{itemize} + +\paragraph{More Keystroke Options} + +All the patches can be played by pressing their associated letter. But additionally you can (at present) {\bf Climb} by pressing the Space Bar, {\bf Retry} by pressing the Return/Enter key, and {\bf Back Up} by pressing Backspace. + +\subsection{Constricting} +\label{constrict} + +\begin{wrapfigure}{r}{3.6in} +\vspace{-2em}\includegraphics[scale=0.27]{ConstrictorPanel.png} +\vspace{-2em} +\caption{The Hill-Climber Panel converted for Constricting.} +\label{constrictorpanel} +\end{wrapfigure} + +The {\it Constrictor} is similar to the Hill-Climber in many ways, yet its dynamics and behavior are quite different. You choose the Constrictor in the same way as the Hill-Climber: by choosing {\bf Hill-Climb} in the {\bf Edit} menu. Then in the Hill-Climb tab, change the ``Method'' combobox to either {\it Constrictor} or to {\it Smooth Constrictor}. + +The Constrictor auditions patches to you in exactly the same way was the Hill-Climber. But you're not given the option of choosing your top three choices. Instead, you're asked to choose which patches you {\it don't like}. This is done by deselecting their checkboxes. Afterwards, you click on the {\bf Constrict} button and those patches will be replaced with recombined versions of the remaining patches. The newly recombined replacements will be moved to the front of the Candidates so you hear them first. + +The idea behind the Constrictor is to start with a set of varied but high-quality patches\,---\,derived from well-vetted factory patches for example. Think of these patches as the outer boundaries of a large region of the space. As you delete patch sounds you don't like, this region slowly begins to collapse, until ultimately it converges to a single patch. + +\paragraph{Initialization} For the constrictor to work, you have to initialize the Candidates with a set of varied patches. You could load these patches one by one by clicking on each Candidates' Options button and choosing {\it Load from File}. But there's an easier way. Load just the first four Candidates this way, via Load from File, and then clicking on the {\bf Reset} button and choose {\bf From First Four Candidates}. The remaining candidates (5...16, or even 5...32) will be generated from recombinations of the first four. If you're doing 32 candidates (that is, you pressed the {\it Big} button), you might consider loading the first six candidates and then choosing {\bf From First Six Candidates}, which gives a better mix. + +\paragraph{Smooth Versus Unsmooth} +The smooth constrictor recombines candidates by interpolating between their metric parameters. The unsmooth constrictor recombines them by simply picking one parameter or the other from the parents. The unsmooth constrictor makes a more varied set of patches. Tests have found that people like the smooth constrictor better, so I might get rid of Unsmooth in the future. + +\paragraph{Using the Constrictor Along with the Hill-Climber} It's not a bad idea to start with a constrictor, constrict down to a single patch or so, and then switch over to the hill-climber and hill-climb from there. It is a {\bf bad} idea to go from the hill-climber to the constrictor because the hill-climber restricts the candidates to a small space and the constrictor will constrict that small space almost immediately. + +\ignore{ +\subsection{How the Hill-Climber Makes New Sounds} + +The Hill-Climber builds new sounds by recombining your top three sound choices and adding some degree of noise (mutation) to them. Let's say that your top three sounds were sounds {\bf A, B,} and {\bf C}, and that your {\it previous} ``A'' from last time is called {\bf Z}. You can also select only two top choices (A and B), or single choice (A). Figure \ref{hcfigure} explains the mechanism by which the 16 new patches are generated through various recombination and mutation mechanisms for the first 16 patches. If you have chosen to use 32 candidate patches, then the second 16 are done exactly like the first 16, except with twice the mutation rate. + +The hill-climber tries to balance coming up with mixtures of your selections as well as things well outside the searched space (balancing so-called {\it exploration} and {\it exploitation}). It also attempts to move further in the direction you had just moved, in the hopes that you'll like even more of what you had selected. + + +% \begin{figure}[t] +% \begin{center} +% \small +% \begin{tabular}{@{}ll@{}} +% \multicolumn{2}{c}{\bf A, B, and C}\\ +% \hline +% \bf Operation&\bf Mutation\\ +% A $+$ B& 1x\\ +% A $+$ C& 1x\\ +% B $+$ C& 1x\\ +% A $+$ (B $+$ C)& 1x\\ +% A $-$ B& 1x\\ +% B $-$ A& 1x\\ +% A $-$ C& 1x\\ +% C $-$ A& 1x\\ +% B $-$ C& 1x\\ +% C $-$ B& 1x\\ +% A $-$ Z& 1x\\ +% B $-$ Z& 1x\\ +% C $-$ Z& 1x\\ +% A& 3x\\ +% B& 3x\\ +% C& 3x\\ +% \end{tabular}\hspace{\fill} +% \begin{tabular}{@{}ll@{}} +% \multicolumn{2}{c}{\bf A, and B}\\ +% \hline +% \bf Operation&\bf Mutation\\ +% A $+$ B& 1x\\ +% A $+$ B& 2x\\ +% A $+$ B& 3x\\ +% A $-$ B& 1x\\ +% A $-$ B& 2x\\ +% B $-$ A& 1x\\ +% B $-$ A& 2x\\ +% A $-$ Z& 1x\\ +% A $-$ Z& 2x\\ +% B $-$ Z& 1x\\ +% B $-$ Z& 2x\\ +% (A $-$ Z) $+$ (B $-$ Z)& 1x\\ +% A& 1x\\ +% A& 2x\\ +% B& 1x\\ +% B& 2x\\ +% \end{tabular}\hspace{\fill} +% \begin{tabular}{@{}ll@{}} +% \multicolumn{2}{c}{\bf A Only}\\ +% \hline +% \bf Operation&\bf Mutation\\ +% A& 2x\\ +% A& 2x\\ +% A& 2x\\ +% A& 2x\\ +% A& 2x\\ +% A& 2x\\ +% A& 3x\\ +% A& 3x\\ +% A& 3x\\ +% A& 4x\\ +% A& 4x\\ +% A& 5x\\ +% A& 5x\\ +% A $-$ Z& 1x\\ +% A $-$ Z& 2x\\ +% (A $-$ Z) $-$ Z& 2x\\ +% \end{tabular} +% \end{center} +% \caption{Operations producing the sixteen new children each hill-climbing step. The operations performed depend on whether the user has made one, two, or three selections. Each operation is followed by a certain degree of mutation. A, B, and C are the (up to) three selections, A being most preferred and C being least. Z is the most preferred selection the {\it previous} iteration. The operation \(X + Y\) means to recombine X with Y, with somewhat of a bias towards X. The operation \(X - Y\) means to find a point on the opposite side of X from Y. The values 2x, 3x etc. aren't {\it stronger} mutation; but rather they are {\it repeated mutation operations}. That is, 3x means to mutate, then mutate again, then mutate again. See Figure \ref{hcfigure}.} +% \label{hctable} +% \end{figure} + + +\begin{figure}[t] +\begin{center} +\includegraphics[width=5in]{HillClimbingBig.pdf} +\end{center} +\caption{Recombination and Opposite-Recombination in the Hill-Climber. Shown are (left to right) basic operations performed when the user has made three (A, B, C), two (A, B), or only one (A) selections. Z is the top selection the previous iteration (the previous iteration's A). Operations are combinations of basic Edisyn recombination procedures. Specifically, \(A + B\) means ordinary recombination between \(A\) and \(B\), weighted towards \(A\). Whereas \(A - B\) means opposite-recombination, finding a point on the other side of \(A\) from the location of \(B\). Each operation is accompanied by numbers in brackets, such as [2, 3]. In this example, this means that two children will be produced using this particular operation: one (2) will be then mutated twice, and the other (3) three times. Recombination and Opposite-Recombination are always done with a weight of 0.75; but mutation is done with a weight specified by the user on the panel.} +\label{hcfigure} +\end{figure} + +When you fire up the Hill-Climber the first time, it doesn't have three candidates yet, nor does it have a Z. So instead what it does is take your current patch and build sixteen sounds as follows: + +\begin{itemize} +\item{1--4}: Four mutated versions of the current patch +\item{5--8}: Four twice-mutated versions of the current patch +\item{9--12}: Four thrice-mutated versions of the current patch +\item{13--16}: Four 4x mutated versions of the current patch +\end{itemize} + +Again, if you have chosen to do 32 patches rather than 16, then the second 16 are done in the same way, but with twice the mutation rate. + +\subsection{How Recombination and Mutation Work} + +Edisyn's patch merging, patch mutation (randomization), nudging, and hill-climber all rely on certain low-level patch manipulation operations to do their magic. These operations are {\bf mutation}, {\bf recombination}, and {\bf opposite-recombination}. + +All three of these operations take a {\bf weight} (a value between 0.0 and 1.0) which specifies how strong an effect the operation will have. Sometimes this weight specified the {\it probability} that a patch parameter will change. Other times the weight influences the {\it amount} the patch parameter will change by. + +Patches are lists of parameters. Each parameter takes one of several forms: + +\begin{itemize} +\item A {\it metric parameter} can take on a value from number range, such as from 0...127. +\item A {\it non-metric} parameter can take on a value from a set of unordered elements: for example, a set of waves, or a set of filter types. +\item A {\it boolean} parameter can take on only two values (0/1, or on/off, of triangle/saw, etc.). Boolean parameters are assumed to be non-metric parameters. +\item A {\it semi-metric} parameter has both a metric range and a non-metric range. For example, a MIDI Channel parameter might have the metric values 1...16 plus the non-metric options OMNI and OFF. +\item Some parameters are {\it immutable}: they will refuse to be modified. These are typically things like patch names. You can make other parameters immutable by turning whether they can be edited on or off (see Section \ref{restriction}). +\end{itemize} + +\begin{figure}[t] +\begin{minipage}{\linewidth} +\begin{minipage}[t]{\linewidth}% +\begin{algorithm}[H] + \caption{~MutateM(Parameter \(P\) with value \(v\); {\it weight})} + \label{alg:example} +\begin{algorithmic} +\STATE \(l, h \leftarrow\) metric min and metric max of \(P\), respectively +\REPEAT +\STATE \(\delta \leftarrow N(0,\, 0.5 \times (\frac{1}{1 - \text{\it weight}} - 1)) \times (h - l) \mod 2\)\hspace{\fill}{\it \(N(\mu, \sigma)\): Normal{\hspace{-0.2em}}} +\UNTIL{\(l - 0.5 < v + \delta < h + 0.5\)} + \STATE{{\bfseries return} \(v + \delta\) rounded to the nearest integer} + % \STATE \(q \leftarrow \text{\it weight} \times (1 + \text{metric max of \(P\)} - \text{metric min of \(P\)})\) + % \STATE \(l \leftarrow \max( v- \lfloor q\rfloor, \text{ metric min of \(P\)})\) +% \STATE \(h \leftarrow \min(v + \lceil q\rceil, \text{ metric max of \(P\)})\) +%\STATE {\bfseries return} a uniform random selection from \([l, h]\) + \end{algorithmic} +\end{algorithm} +\end{minipage}\\ +\begin{minipage}[t]{\linewidth} +\begin{algorithm}[H] + \caption{~Mutate(Parameter \(P\) with value \(v\); {\it weight})} + \label{alg:mutate} +\begin{algorithmic} + \IF{\(P\) is metric} + \STATE{\bfseries return } MutateM(\(P, v\), {\it weight}) + \ELSIF{\(P\) is semi-metric and \(v\) is a metric value} + \IF{probability 0.5} + \STATE {\bfseries return } MutateM(\(P, v\), {\it weight}) + \ELSIF{probability {\it weight}} + \STATE {\bfseries return} a random \(p_{\text{non-metric}} \in P\) + \ENDIF + \ELSIF{\(P\) is semi-metric and \(v\) is not a metric value} + \IF{probability {\it weight}} + \IF{probability 0.5} + \STATE{\bfseries return} a random \(p_{\text{metric}} \in P\) + \ELSE + \STATE {\bfseries return} a random \(p_{\text{non-metric}} \in P\) + \ENDIF + \ENDIF + \ELSIF{probability {\it weight}} + \STATE {\bfseries return} random \(p_{\text{metric}}\!\in P\) + \ENDIF + \STATE {\bfseries return} \(v\) + \end{algorithmic} +\end{algorithm} +\end{minipage} +\end{minipage} +\caption{The {\bf Mutate} function, which calls the {\bf MetricMutate} sub-function as necessary for metric parameters or semi-metric parameters in the metric range. The ``metric min'' and ``metric max'' are the maximum and minimum values in the metric range of parameter \(P\). } +\label{mutate} +\end{figure} + +\paragraph{Mutation} +When a patch {\bf A} is {\it mutated}, each of its parameters is modified with some degree of random noise, or with some probability. Figure~\ref{mutate} shows the procedure for mutating a single parameter \(P\) in a patch. The parameter has the current value \(v\) and the user-specified {\it weight}. The new parameter value is returned. Mutation is used in patch randomization in nudging, and in Hill-Climbing. + +Notice that if \(v\) is a metric value, and the algorithm has decided it will stay a metric value, then it uses a special sub-procedure, {\it MetricMutate}, to mutate the value to something relatively near to the original; the distance is determined by the weight. Otherwise generally new (metric, non-metric) values are chosen completely at random with a certain probability based on the weight. + + +\begin{figure}[t] +\begin{minipage}{\linewidth} +\begin{minipage}[t]{\linewidth}% +\begin{algorithm}[H] + \caption{~Recombine(Param \(P\) with values \(v, w\); {\it weight})} + \label{alg:recombine} +\begin{algorithmic} + \IF{both \(v\) and \(w\) are metric values in \(P\)} + \STATE \(q \leftarrow v - \text{\it weight} \times (v - w),\)\quad rounded towards \(w\) + \STATE {\bfseries return} a random uniform selection from \([v, q ]\) + \ELSIF{probability {\it weight}} +\STATE {\bfseries return} \(w\) + \ENDIF + \STATE {\bfseries return} \(v\) + +% \IF{probability {\it weight}} +% \IF{\(v\) and \(w\) are both metric values in \(P\)} +% \STATE {\bfseries return } a uniform random selection from \([v, w]\) +% \ELSIF{probability 0.5} +% \vspace{-1.2em}\STATE\hspace{9.5em}~~{\bfseries return \(w\)} +% \ENDIF +% \ENDIF +% \STATE {\bfseries return} \(v\) + \end{algorithmic} +\end{algorithm} +\end{minipage}\\ +\begin{minipage}[t]{\linewidth} +\begin{algorithm}[H] + \caption{~Opposite(Param \(P\) with values \(v, w\); {\it weight})} + \label{alg:opposite} +\begin{algorithmic} + \IF{both \(v\) and \(w\) are metric values in \(P\)} + \STATE \(q \leftarrow v + \text{\it weight} \times (v - w),\)\quad rounded away from \(w\) + \STATE \(q \leftarrow \text{min}(\text{max}(q, \text{ metric min of \(P\)}), \text{ metric max of \(P\)})\) + \STATE {\bfseries return} a random uniform selection from \([v, q]\) + \ENDIF + \STATE {\bfseries return} \(v\) + \end{algorithmic} +\end{algorithm} +\end{minipage} +\end{minipage} +\caption{The {\bf Recombine} and {\bf Opposite} functions. Each take a parameter with {\it two} values \(v\) and \(w\) (one from each patch being recombined), plus a user-specified {\it weight}. Additionally the Opposite function takes a boolean argument, {\it flee}, which indicates whether Opposite is being presently used to flee away from \(w\). The ``metric min'' and ``metric max'' are the maximum and minimum values in the metric range of parameter \(P\).} +\label{recombine} +\end{figure} + +% this little dance is because \paragraph won't work right here and I'm not sure why +\vspace{1em}\noindent{\bf Recombination}\quad +This takes two values \(v\) and \(w\) for a given parameter \(P\) and returns a new value for \(v\). If both values are metric, then a value is selected between them with a certain probability. Else \(v\) either stays as \(v\) or changes to \(w\) with a certain probability. See Figure \ref{recombine}. Recombination is used in the patch-merge operation, as well as forming the ``plus'' operation in the Hill-Climbing procedure (see Figure~\ref{hcfigure} and Table~\ref{hctable}). + +% this little dance is because \paragraph won't work right here and I'm not sure why +\vspace{1em}\noindent{\bf Opposite Recombination}\quad +This is used to find a new patch \(Q\) which is on the {\it other side} of patch \(P\) from where patch \(S\) is. There are two reasons you might want to do this. One obvious reason is because you want to essentially move \(P\) away from \(S\) (so \(P\) becomes \(Q\)). This is called {\it fleeing}: it's used in Edisyn for {\it nudging away} from a target patch. + +The other reason is because you have recently moved from \(S\) to \(P\), and now you'd like to move \(P\) even {\it further} in that direction. This is the ``minus'' operation done the Hill-Climbing procedure (see Figure~\ref{hcfigure}). You'll note from Figure \ref{recombine} that the Opposite procedure takes a {\it flee} argument to distinguish between your two reasons. This largely influences what will happen if the two parameter values are the same.\footnote{Note that we assume that all metric parameters are integers, not real-valued}. +} +\section{Writing a Patch Editor} + +So you want to write a patch editor? They're not easy. But they're fun! Here are some hints. + +\subsection{Step One: Understand What You're Getting Into} +\label{inconsistent} + +Make sure you understand that Edisyn can only go so far to help you in writing a patch editor: but synthesizer sysex world is an inconsistent, buggy mess. + +For example, the Waldorf Blofeld's multimode sysex is undocumented and must be reverse engineered. The PreenFM2 bombs when it receives out-of-range values over NRPN, but happily sends them to you. The PreenFM2 has sysex files for its patches, but they are undocumented and are basically unusable memory dumps of IEEE 754 floating-point arrays. The Yamaha TX81Z requires not one but {\it two} separate sysex patch dumps in a row, in order to be backward compatible with an earlier synth family nobody cares about: it also is incapable of writing a patch (likewise the PreenFM2). And it too bombs if you send it invalid data. The Kawai K4's sysex documentation is riddled with incredible numbers of errors. The Matrix 1000 accepts patch names but doesn't store or emit them: it just ignores them. Synths often pack multiple parameters into the same byte, making it impossible to update just a single parameter: you have to update five at a time. There are multiple different strategies for packing data of size 8 bits and up. Some synths, like the Futuresonus Parva, DSI Prophet '08, and Yamaha DX7, are highly regular in their format, while others, like the infamous Korg Microsampler, require custom tables for nearly every parameter. + +Below is a little table of the current patch editors for Edisyn, and various Edisyn capabilities that they can or cannot take advantage of. + +\newcommand\samefootnote{\addtocounter{footnote}{-1}\footnotemark} +\newcommand\backfootnote[1]{\addtocounter{footnote}{-#1}\footnotemark\addtocounter{footnote}{#1}\addtocounter{footnote}{-1}} +\newcommand\cm{\checkmark} + +\begin{center} +{\small +\begin{tabular}{rllllllllll} +& +\begin{sideways}Send Parameter\end{sideways}& +\begin{sideways}Receive Parameter\end{sideways}& +\begin{sideways}Request Specific Patch\end{sideways}& +\begin{sideways}Request Current Patch\end{sideways}& +\begin{sideways}Send to Current Patch\end{sideways}& +\begin{sideways}Send to Specific Patch\end{sideways}& +\begin{sideways}Write to Specific Patch\end{sideways}& +\begin{sideways}Change Mode\end{sideways}& +\begin{sideways}Receive Error or Ack\end{sideways}& +\begin{sideways}Standard Sysex File\end{sideways}\\[0.5em] +\hline\\[-0.5em] +%Futuresonus Parva&{\cm}&{\cm}&{\cm}&{\cm}&{\cm}&&{\cm}&&&\\ +DSI Prophet '08&{\cm}&{\cm}&{\cm}&{\cm}&{\cm}&{\cm}&{\cm}& & &{\cm} \\ +E-Mu Morpheus / UltraProteus&{\cm}& &{\cm}& & &{\cm}&{\cm}& & &{\cm} \\ +Waldorf Microwave II/XT/XTk&{\cm}&{\cm}&{\cm}&{\cm}*&{\cm}&{\cm}&{\cm}&{\cm}& &{\cm} \\ +Waldorf Blofeld&{\cm}*&{\cm}*&{\cm}&{\cm}*&{\cm}&{\cm}&{\cm}& & & {\cm} \\ +Kawai K1/K1r/K1m&{\cm}*& &{\cm}& & & &{\cm}& &{\cm}&{\cm} \\ +Kawai K4/K4r&{\cm}*& &{\cm}& &{\cm}&{\cm}&{\cm}& &{\cm}&{\cm} \\ +Kawai K5/K5m&{\cm}&{\cm}&{\cm}& & &{\cm}&{\cm}& &{\cm}&{\cm} \\ +Yamaha DX7&{\cm}&{\cm}& & & & {\cm}& & & &{\cm}* \\ +Yamaha TX81Z&{\cm}*& & &{\cm}&{\cm}& & & & &{\cm} \\ +PreenFM2&{\cm}&{\cm}&{\cm}&{\cm}&{\cm}&{\cm}& & & & \\ +Oberheim Matrix 1000&{\cm}&{\cm}&{\cm}& &{\cm}*&{\cm}*&{\cm}& & &{\cm} \\ +Korg Microsampler&{\cm}*&{\cm}*& & & & & & & & \\ +Korg SG Rack&&&{\cm}&{\cm}&{\cm}&{\cm}&{\cm}&{\cm}&&{\cm}\\ +Korg Wavestation&{\cm}&&{\cm}&{\cm}*&{\cm}*&{\cm}&{\cm}&{\cm}*&&{\cm}\\ +\end{tabular} +} +\\[1em] +* {\it With significant caveats or restrictions} +\end{center} +\vspace{1em} + +I particularly love how the Korg Microsampler and the Korg SG are disjoint in their abilities; yet they're from the same company. Long story short, you'll probably have to do a lot of customization. I've tried to provide many customization options in Edisyn. If you need something Edisyn doesn't provide, contact me. + +\subsection{Step Two: Set Up the Development Environment} + +Still not scared away? Okay, we'll start by getting Edisyn set up for development. Probably the easiest way to fire up Edisyn for purposes of testing is as a build directory. You just need to add two items to your CLASSPATH: + +\begin{enumerate} +\item The {\tt coremidi4j-1.1.jar} file, located in the {\tt libraries/} folder (you can move it where you like). This jar file contains the CoreMidi4J library, which enables sysex to work properly on Macs (you'll need it for non-Macs too). +\item The {\tt trunk} directory. This parent directory holds the {\tt edisyn} package. Or if you like, make some other directory {\tt foo} and move (or link) {\tt edisyn} into that directory, then add {\tt foo} to your CLASSPATH. +\end{enumerate} + +Now you can compile Edisyn with\ \ \ {\tt javac~edisyn/*.java~edisyn/*/*.java~edisyn/*/*/*.java}\\ +You can then run Edisyn as\ \ \ {\tt java edisyn.Edisyn} + +\subsection{Step Three: Create Files} + +Let's say you're adding a single (non-multimode) patch editor for the Yamaha DX7. + +Make a directory called {\tt edisyn/synth/yamahadx7}. This directory will store your patch editor and any auxiliary files. Next copy the file {\tt edisyn/synth/Blank.java} to {\tt edisyn/synth/yamahadx7/YamahaDX7.java}. That'll be your patch editor code. Also copy the file {\tt edisyn/synth/Blank.html} to {\tt edisyn/synth/yamahadx7/YamahaDX7.html}. This will be the ``About'' documentation for your file. You'll eventually fill it out. + +Modify the {\tt YamahaDX7.java} file to have the proper class name and package. + +Edit the {\tt edisyn/Synth.java} file. In that file there is an array called: + +\begin{verbatim} +public static final Class[] synths +\end{verbatim} + +Add to this array your class: + +\begin{verbatim} + edisyn.synth.yamahadx7.YamahaDX7.class, +\end{verbatim} + +Now Edisyn knows about your (currently nonexistent) patch file. + +Finally, implement the {\tt getSynthName()} and {\tt getHTMLResourceFileName} methods in your class file, along these lines: + +\begin{verbatim} +public static String getSynthName() { return "Yamaha DX7"; } +public static String getHTMLResourceFileName() { return "YamahaDX7.html"; } +\end{verbatim} + + +\subsection{Step Four: Get the UI Working} + +This is mostly writing the class constructor and subsidiary functions. Typically you will create one {\tt SynthPanel} for each tab in your editor. A SynthPanel is little more than a JPanel with a black background: you can lay it out however you like. However Edisyn typically lays it out as follows: + +\begin{enumerate} +\item At the top level we have a {\bf VBox}. This is a vertical Box to which you can add elements conveniently. You can also designate an element to be the {\bf bottom} of the box, meaning it will take up all the remaining vertical space. +\item In the VBox we will place one or more {\bf Categories}. These are the large colorful named regions in Edisyn (like ``LFO'' or ``Oscillator''). +\item Typically inside a Category we'd put an {\bf HBox}. This is a horizontal box to which you can add elements. You can also designate an element to be the {\bf last item} of the box, meaning it will take up all the remaining horizontal space. By doing this, the Category's horizontal colored line nicely stretches the whole length of the window. +\item Inside the HBox you put your widgets. You might lay them out with additional VBoxes and HBoxes as you see fit. It's particularly common to one or more small widgets (check boxes, choosers) in a VBox, which will cause them to be top-aligned rather than vertically centered as they would if they were stuck directly in the HBox. It's helpful to look at existing patch editors to see how they did it. +\item If you need multiple rows, you should put a VBox in the Category, and then put HBoxes inside of that. +\item You might have multiple Categories on the same row. To do this, just put them in an HBox. Make sure the final Category is designated to be the Last Item of the HBox. You'd put this HBox in the top-level VBox instead of the Categories themselves. +\end{enumerate} + +The first category is the {\bf Synth Category}. It is typically named the same as {\tt getSynthName()}, its color is {\tt edisyn.gui.Style.COLOR\_GLOBAL}, and contains the patch name and patch/bank information, and perhaps a bit more (for example, Waldorf synthesizers have the ``category'' there too). + +To the right of the first category is usually (but not always) various global categories. They're usually {\tt edisyn.gui.Style.COLOR\_A}. + +If you have additional categories, you might distinguish them using {\tt edisyn.gui.Style.COLOR\_B}, and eventually {\tt edisyn.gui.Style.COLOR\_C}. + +You can lay out the rest of the categories as you see fit. + +\paragraph{Think about Parameters} + +Synthesizer parameter values will be stored in your Synth object's {\bf Model}. These parameters will be stored in your synth's {\bf Model} object. Each parameter has a {\bf Key}. Edisyn traditionally names the keys all lower case, plus numbers, with no spaces or hyphens or underscores, and tries to keep the keys fairly similar to how your synth sysex manual calls them. They're usually described with a category descriptor (such as {\tt op3} and then the parameter name proper (such as {\it envattack}), resulting in the final key name {\it op3envattack}. Various global parameters are just the parameter name: for example, it's standard in Edisyn that the patch name be just called {\it name}, the patch number is called {\it number}, and the bank number is called {\it bank}. + +Often parameters (as set by widgets) are exactly the same as the various elements you send and receive to the synthesizer. But sometimes they're not. Many synthesizers pack multiple parameters (like LFO Speed \(+\) Latch) together into a single variable, which is very irritating. You want to lay out what the {\it real} parameters of your synthesizer are, that the user would be modifying, not what you'd be packing and sending to the synth. + +Another issue is how your synthesizer interprets values sent over sysex or NRPN. Consider BPM for a moment. Perhaps your synthesizer has BPM values of 20...300, and there are missing values (for example, there's no 21). The actual values are mapped to the numbers 0...127. What values should you store? In my patch editors, I store the values in the model as 0...127, which makes it easy to emit them. But then I have to have an elaborate conversion function to map them to 20...300 for display on-screen. + +Also some synthesizers have holes in their ranges. For example, they might permit the values 0...17 and the values 20...100, but do not permit 18 and 19. What to do then? You probably ought to compact them to be contiguous between some min and max: for example, you might compact it to 0...98. When displayed, use a custom displayer, and when emitting or parsing them, you'll have to map them to your internal representation accordingly. + +In summary: your internal parameters ought to have contiguous ranges and should make sense from the user's perspective and not the synthesizer's weird parsing perspective. + +So how to set parameters? You usually don't add the key yourself, though you could. Instead, normally you tell the widget the name of the parameter it's modifying (the key), and it adds it to the model on its own. Parameters are either {\bf strings} or are {\bf numbers}. Numerical parameters all have a {\bf min} and a {\t max} value, inclusive: usually the widget will set those for you. They also may have a {\bf MetricMin} and {\bf MetricMax} value, and you may need to set those manually. + +MetricMin and MetricMax work like this. Some numerical parameters are {\bf metric}, meaning they're a range of numbers where the order matters, such as 0--127. Other numerical parameters are {\bf categorical} (or ``non-metric''), meaning that the numbers just represent an ID for the parameter. For example, a list of wavetables is categorical: it doesn't {\it really} matter that wavetable 0 is ``HighHarm3'': it's just where it's stored in your synth. + +Edisyn is smart about mutating and recombining metric parameters, but for non-metric ones it just picks a new random setting. Sometimes your parameters are {\it both} metric and non-metric. For example, some parameter might have the values 1--32 plus the non-metric values ``off'' (0), ``uniform'' (17), and ``multi'' (18), or whatever. In this case, your min is 0 and your max is 18. But your {\it metric min} is 1 and your {\it metric max} is 16. This tells Edisyn that values outside the metric min / metric max range should be treated as non-metric.\footnote{What if your synth has metric values on the outside and non-metric value on the inside? Edisyn can't handle that. Thankfully I've not seen it yet.} If you have this situation, you'll need to set the Metric Min and Metric Max manually. + +Parameters can be declared {\bf immutable}, meaning Edisyn can't mutate them or cross them over at all. Also, all string parameters are automatically immutable. You'll need to declare the others. + +\paragraph{Copying and Distributing Parameters} + +If your synth has multiple copies of the same category (for example, multiple LFOs), you can {\it copy} parameters wholesale from one category to another. To do this, parameters must obey a certain convention. Specifically, parameters in a category must all start with the same {\it preamble}, which must contain no digits, followed by a {\it category number}, which must be all digits. After that, you can have whatever you like. For example, {\tt lfo1rate} or {\tt osc14attack}. If you have four LFO categories, their category numbers might be 1...4, say. After you have set up your parameters appropriately, you can turn on copy and paste in a given category by calling {\bf makePasteable(\textit{preamble})}, passing in the preamble (not the category number). + +Categories also often contain multiple instances of a given parameter. For example, a step sequencer category might contain 16 steps. You can {\it distribute} values to all such parameters if you follow the a similar convention, specifically, parameters should start with a {\it preamble}, and then the first string of digits will refer to the index of the parameter. For example, if your step sequencer had {\tt seq} as its preamble, perhaps you might have {\tt seqstep1} through {\tt seqstep16}. You an have additional text, such as {\tt seqstep1attack} or whatnot. After you have set up your parmaters, you can turn on distribution by calling {\bf makeDistributable(\textit{preamble})}, passing in the preamble. + +Your category can also do both of these things. In this case, all parameter names should obey the copy/paste convention, and distributable parameters should have a {\it second} string of digits somewhere later in the parameter name which refers to the parameter index. For example, you might have {\tt seq1step1} through {\tt seq1step16} for sequencer 1, and {\tt seq2step1} through {\tt seq2step16} for sequencer 2. + +Finally, by default categories can be {\it reset}. It's probably wise to turn this off in the global category. This is done by calling {\bf makeUnresttable()}. + +\paragraph{Common Widgets} + +Edisyn has a number of widgets available. Most widgets are associated with a single parameter (a ``key''). There is no reason you can't have multiple widgets associated with the same key: when that parameter is updated, all associated widgets are updated. + +The most common widgets are: + +\begin{itemize} +\item {\bf StringComponent}\quad This is the only String widget. It's used for patch names. For a patch name, you typically implement it like this: +\begin{verbatim} +String key = "name"; // the key in the model +String instructions = "Name must be up to 10 ASCII characters."; +JComponent comp = new StringComponent("Patch Name", this, "name", maxLength, instructions) + { + public String replace(String val) + { + return revisePatchName(val); + } + public void update(String key, Model model) + { + super.update(key, model); + updateTitle(); + } + }; +\end{verbatim} + +In conjunction with this, you will want to override the {\bf revisePatchName(...)} method in your Synth subclass. This method modifies a provided name and returns a corrected version. The default version, which you might call first (via super), removes trailing whitespace. You can then revise incorrect characters, length, and so on. + +\item {\bf Chooser}\quad This is a pop-up menu or combo box, and it's a numerical component. You provide it with an array of strings representing the parameter values 0...{\it n}. For example, you might set up a wavetable chooser as: + +\begin{verbatim} +String key = "wave"; // the key in the model +String[] params = WAVE_OPTIONS; // this is an array of wave names elsewhere +JComponent comp = new Chooser("Wave", this, key, params); +\end{verbatim} + +There's an option to add images to the chooser's menu: + +\begin{verbatim} +public static final ImageIcon[] MY_WAVE_ICONS = + { + new ImageIcon(YamahaDX7.class.getResource("Wave1.png")), + new ImageIcon(YamahaDX7.class.getResource("Wave2.png")), + ... // and so on + }; +String key = "wave"; // the key in the model +String[] params = WAVE_OPTIONS; // this is an array of wavetable names elsewhere +JComponent comp = new Chooser("Wave", this, key, params, MY_WAVE_ICONS); +\end{verbatim} + +These PNG files would be stored in your {\tt edisyn/synth/yamahadx7/} directory. They should be no taller than 16 pixels high: OS X refuses to display comboboxes with icons taller than that. + + +\item {\bf Checkbox}\quad This is a simple checkbox. By default it's on, but there's a setting to have it by default be off. On is 1 and Off is 0 as stored in the model. + +\begin{verbatim} +String key = "arpeggiatorlatch"; // the key in the model +JComponent comp = new CheckBox("Arpeggiator Latch", this, key); +\end{verbatim} + +The's a bug in OS X which mis-measures the width of the string needed, so you might see ``Arpeggia...'' instead of ``Arpeggiator Latch'' on-screen. To fix this, just add a tiny bit to the width: usually one or two pixels are enough: + +\begin{verbatim} +String key = "arpeggiatorlatch"; // the key in the model +JComponent comp = new CheckBox("Arpeggiator Latch", this, key); +((CheckBox)comp).addToWidth(1); +\end{verbatim} + +\item {\bf LabelledDial}\quad This is a labelled dial representing a collection of numbers from some min to some max. + +\begin{verbatim} +int min = 1; +int max = 16; +Color color = edisyn.gui.Style.COLOR_A; // Make this the same color as the enclosing Category +JComponent comp = new LabelledDial("MIDI Channel", this, "midichannel", color, min, max); +\end{verbatim} + +It's common that you need more lines in your label. Perhaps you might say: + +\begin{verbatim} +int min = 1; +int max = 16; +Color color = edisyn.gui.Style.COLOR_A; // Make this the same color as the enclosing Category +JComponent comp = new LabelledDial("Incoming", this, "midichannel", color, min, max); +((LabelledDial)comp).addAdditionalLabel("MIDI Channel"); +\end{verbatim} + +You can add additional (third, fourth, ...) labels too. Note that you can change the first label text later on (with {\bf setLabel(...)}) but you can't change the label text of additional labels. + +It is very common to need a custom string display for certain numbers in the center of the dial. You can do it like this: + +\begin{verbatim} +int min = 0; +int max = 17; +Color color = edisyn.gui.Style.COLOR_A; // Make this the same color as the enclosing Category +JComponent comp = new LabelledDial("MIDI Channel", this, "midichannel", color, min, max) + { + public String map(int val) + { + if (val == 0) return "Off"; + else if (val == 17) return "Omni"; + else return "" + val; + } + }; +\end{verbatim} + +Note that if you're just trying to subtract a certain amount from the dial, for example, to display the values 0...127 as the values -64...63, then there's a constructor option on LabelledDial for this: + +\begin{verbatim} +new LabelledDial("Pan", this, "pan", color, 0, 127, 64) // subtracts 64 before displaying +\end{verbatim} + +This brings us to the discussion of {\it symmetry}. Sometimes you want the dial to be symmetric looking, and sometimes not. Edisyn tries hard to see to it that, whenever possible, the ``zero'' position on the dial is vertically directly above or directly below the center of the dial. For example, a symmetric dial going from \(-100\) to \(+100\) would have zero at the top: and a dial going from 0 to 127 would have zero at the bottom (this second case results in Edisyn's unusual ``C''-shaped dials). The ``zero'' position doesn't always mean 0: it should be the notional identity for the dial. For example, a Keytrack dial might have 100\% be the identity position. + +By default Edisyn's dials assume that the zero position is at the beginning of the dial, resulting in the ``C'' shape. Because a great many synthesizers go from 0...127 or from 0...100, if you use the aforementioned constructor option to subtract either 64 or 50 from the dial, Edisyn will automatically make it look symmetric. + +\begin{minipage}{\linewidth} +\begin{wrapfigure}{r}{0.5in} +\vspace{-1em}\includegraphics[scale=0.5]{6.png}\\ +\includegraphics[scale=0.5]{7.png} +\vspace{-1em} +\end{wrapfigure} + +Sometimes you need to customize the orientation in order to keep the zero position vertically centered. For example Blofeld's Arpeggiator has a variety of dials which aren't {\it quite} symmetric, because there are some unusual options at the start, as shown on the top figure at right. But even worse: the Kawai K4 Effects patch has a number of dials which look like a {\it reversed} ``C'' because of so many additional options loaded at the end of the dial, as shown on the bottom figure. + +\vspace{0.5em}You can customize the orientation in two ways. First, if you override LabelledDial's {\bf isSymmetric()} method to return {\bf true}, then the dial will display itself as fully symmetric. Second, you could override LabelledDial's {\bf getStartAngle()} method to return the desired angle of the start (leftmost) position of your curve. The default is 270 (the ``C''), and when fully symmetric it's \mbox{90 + (270 / 2).} +\end{minipage} + +\vspace{0.5em}When the user double-clicks on a LabelledDial, try to have the LabelledDial go to some default position. This is often the ``zero'' position: but sometimes it's not. At any rate, it's almost always most common position the user would want, whatever that is. By default the ``default position'' is the first position if asymmetric, and the center position if symmetric. You can change the default position by overriding LabelledDial's {\bf getDefaultValue()} method to return a different value. + +Last but not least! If you have a mixture of metric and non-metric values (for example, 0=``Off'', 1...32 = 1...32, and 33=``Uniform''), you will need to modify the MetricMin and MetricMax declarations. Normally LabelledDial declares MetricMin to be the same as Min and MetricMax to be the same as Max. But in this example, your minimum metric value is 1 and your maximum metric value is 32. + +\begin{verbatim} +getModel().setMetricMin("whateverkey", 1); +getModel().setMetricMax("whateverkey", 32); +\end{verbatim} + +It sometimes happens that {\it none} of the LabelledDial values should be thought of as metric. For example, a previous code example, we were using the LabelledDial to select the MIDI Channel. Now, channels aren't metric: they're just 16 unique labels for channels which happen to be numbers. In this case, we should remove the metric min and max entirely, so Edisyn considers the entire range to be non-metric. To do this, we say: + +\begin{verbatim} +getModel().removeMetricMinMax("midichannel"); +\end{verbatim} + + +\item {\bf IconDisplay}\quad This displays a different icon for each value in your model. You can't change the values by clicking or dragging on an IconDisplay: instead, use a separate LabelledDial or Chooser. + +\begin{verbatim} +ImageIcon icons = MY_ALGORITHM_ICONS; +JComponent comp = new IconDisplay("Algorithm Type", icons, this, "algorithmtype"); +\end{verbatim} + +Your images can be PNG or JPEG files: I suggest PNG. You might create an instance variable like this: + +\begin{verbatim} +public static final ImageIcon[] MY_ALGORITHM_ICONS = + { + new ImageIcon(YamahaDX7.class.getResource("Algorithm1.png")), + new ImageIcon(YamahaDX7.class.getResource("Algorithm2.png")), + ... // and so on + }; +\end{verbatim} + +These PNG files would be stored in your {\tt edisyn/synth/yamahadx7/} directory. + + +\item {\bf KeyDisplay}\quad This displays a keyboard. You specify the min and max keys (which {\it must} be white keys), and a transposition (if any) between keys and the underlying MIDI notes actually generated. When the user chooses a key, the KeyDisplay will update a value 0...127 corresponding to the equivalent MIDI note value. + +The KeyDisplay can update {\it dynamically} or {\it statically}. When dynamic, then every time you scroll through the display and a note is highlighted, the model is updated. When static, the model is only updated when a note is finally chosen and the user has released the mouse button. To set this, use {\bf setDynamicUpdate(...)}. + +You will probably want your KeyDisplay to update in concert with a LabelledDial. This is easy: just set them to the same key in the model. but synthesizers are inconsistent in how they describe notes, because MIDI didn't specify a notation. For example, MIDI note 0 is ``C -2'' in Yamaha's notation (also adopted by Kawai and some others), or it is ``C -1'' in {\it Scientific Pitch Notation} (or SPN\footnote{... or {\it American Scientific Pitch Notation}(ASPN), or {\it International Pitch Notation} (IPN). They're all pretentious names.}), or just play ``C 0'' in simple MIDI notation. You can specify this by calling the method {\bf setOctavesBelowZero(...)}. + +In some cases you might wish to be notified whenever the user {\it clicks} on a key, or drags to it, rather than when the key actually is updated (which might only happen on button release). Typically this happens because you want to actually play the note so the user gets some feedback. To be notified of this, just override the method {\bf userPressed(...)}. + + +\item {\bf PushButton}\quad This doesn't maintain a parameter at all: it's just a convenience cover for JButton. You see it in Multimode patches where pressing it will pop up an equivalent Single patch (it's usually called ``Show''): + +\begin{verbatim} +JComponent comp = new PushButton("Show") + { + public void perform() + { + // do your stuff here + } + }; +\end{verbatim} + +Popping up new synth panels from a multimode panel is complex. Take a look at how {\tt edisyn/synth/waldorfmicrowavext/WaldorfMicrowaveXTMulti.java} does it. + +\item {\bf PatchDisplay}\quad This displays your patch and bank in a pleasing manner.\footnote{{\it Why is PatchDisplay so elaborate? Why not just use a JLabel or something?}\quad Originally PatchDisplay did other complex things like change color. Now it doesn't.} + +\begin{verbatim} +String numberKey = "number"; // typically or null if you have no patch numbers +String bankKey = "bank"; // typically, or null if you have no bank numbers +int numberOfColumns = 10; // for example +JComponent comp = new PatchDisplay(this, "Patch", bankKey, numberKey, numberOfColumns) + { + public String numberString(int number) { "" + number} // format as you like + public String bankString(int bank) { "" + bank} // format as you like + }; +\end{verbatim} + +\item {\bf EnvelopeDisplay}\quad This displays a wide variety of envelopes. Envelopes are drawn as a series of points, and between every successive pair of points we draw a line. You will provide the EnvelopeDisplay with several arrays defining the coordinates of those points. + +There are two main kinds of envelopes your synthesizer might employ. First, your synthesizer might define parameters (like attack) in terms of the {\it height} of the attack and also the {\it amount of time} necessary to reach that height. This is intuitive to draw, but in fact many synthesizers don't do it that way. Instead, some define it in terms of the {\it height} of the attack and the {\it rate of change} (or slope, or angle). In the first case, the height of the attack has no bearing on how long it takes to reach it. But in the second case, the amount of time to reach the attack depends on both the height and on the rate. This is even further complicated by some synthesizers (like Yamaha's) which use rate, but compute it not in terms of angle, but in terms of (essentially) 90 degrees {\it minus} the angle. Thus a steeper rate is a {\it lower number}. You will need to figure out what your synthesizer does exactly. + +Let's say your synth does the easy thing and computes stuff in terms of height and amount of time. Then you set up an Envelope Display with four elements: + +\begin{itemize} +\item An array of keys (some of which can be null) of the parameters which define the {\it amount of time} for each segment. If a key is null, the parameter value is assumed to be 1.0. +\item An array of keys (some of which can be null) of the parameters which define the {\it height} for each segment. If a key is null, the parameter value is assumed to be 1.0. +\item An array of constant doubles which will be multiplied against the time parameters. You want these constants to be such that, when the time parameters are at their maximum length, their values, multiplied by these constants, will sum to no more than 1.0 +\item An array of constant doubles which will be multiplied against the height parameters. You want these constants to be such that, when the any given height parameter is maximum, when multiplied against the constant it will be no more than 1.0. +\end{itemize} + +Here's how you'd make an Envelope Display for an ADSR envelope where each of the values varies 0...127: + +\begin{verbatim} +String[] timeKeys = new String[] { null, "attack", "decay", null, "release" }; +String[] heightKeys = new String[] { null, "attackheight", "sustain", "sustain", null }; +double[] timeConstants = new double[] { 0.0, 0.25 / 127, 0.25 / 127, 0.25, 0.25 / 127 }; +double[] heightConstants = new double[] { 0.0, 1.0 / 127, 1.0 / 127, 1.0 / 127, 0.0 }; +JComponent comp = new EnvelopeDisplay(this, Color.red, "ADSR", + timeKeys, heightKeys, timeConstants, heightConstants); +\end{verbatim} + +Notice that {\tt "sustain"} is used twice: thus the line stays horizontal; and furthermore its time constant is fixed to 0.25 so it always takes up 1/4 of the envelope space. Also notice that in this example the beginning and end of the ADSR envelope are fixed to 0.0 height. That doesn't have to be the case. And maybe you don't have an attack height: it's always full-on attack. Then you'd say: + +\begin{verbatim} +String[] heightKeys = new String[] { null, null, "sustain", "sustain", null }; +double[] heightConstants = new double[] { 0.0, 1.0, 1.0 / 127, 1.0 / 127, 0.0 }; +\end{verbatim} + +It's possible that your envelope isn't always positive: it can go negative. The EnvelopeDisplay normally assumes that your parameters are all positive numbers (like 0--127), but it does allow to draw a line indicating where the X axis should be, via the {\bf setAxis(...)} method. See the fourth example in Figure \ref{envelopedisplays}. You can also indicate that your Y values are {\it signed}, which means that when multiplied against their respective constants, they will range from -1...1 instead of from 0...1. This is done with {\bf setSigned(...)}. + +You also can also tell the EnvelopeDisplay to draw a vertical line at some key position and a dotted line at another, using the methods {\bf setFinalStageKey(...)} and {\bf setSustainStageKey(...)} respectively (these are named after their use in the Waldorf Microwave XT). These keys should specify the {\it stage number} (the point) where the line is drawn. For example, if the sustain stage key's value is 4, then the line should be drawn through point number 4 (zero-indexed) in the envelope. See the third example in Figure \ref{envelopedisplays}. + +You can also specify two intervals with start and stop keys respectively. At present the EnvelopeDisplay supports two intervals. These are set up with {\bf setLoopKeys(...)}. These keys should specify the {\it stage number} (the point) where the intervals are marked. For example, if the interval end's key value is 4, then the end should be marked exactly at point number 4 (zero-indexed) in the envelope. Again, see the third example in Figure \ref{envelopedisplays}. + +You can also postprocess the sustain stage, final stage, or loop keys with {\bf postProcessLoopOrStageKey(...)}. This function takes a key and its value, and returns a revised value, perhaps to add or subtract 1 from it. + +What if your synth uses angles/rates/slopes rather than time intervals? For example, the Waldorf Blofeld does this. To handle this situation, we add an additional array of double constants called {\it angles}. It works like this. The height keys and height constants are exactly as before. And {\tt timeConstants[0]} still defines the x position of the first point in the envelope, as before. But the other time constants work differently. + +Specifically, to compute the X coordinate of the next point, we take its key value and multiply it by the corresponding angle, and then take the absolute value. This tells us the {\it positive angle} of the line. Angles can never be negative: whether the line has a positive or negative slope is determined entirely by the relative position of the height keys. + +Since angles can and will create very strung-out horizontal lines, the remaining time constants tell us the {\it maximum length} of a line: these again should sum to 1.0. + +Angles/rates create weird idiosyncracies you'll have to think about. For example, below is the code for the Blofeld's ADSR envelope. As the Sustain gets higher, the Release gets longer but the Decay gets shorter, because the synth is basing this envelope on {\it rate} and not {\it time}.\footnote{Edisyn no longer displays this way for the Blofeld, because although the Blofeld indeed follows angle/rate, for large values the Blofeld's functions start getting close to following time. The problem is that while the Blofeld documentation acknowledges that it follows angle/rate, the Blofeld's {\it screen} incorrectly displays envelopes following time! When I wrote this documentation I was using angle/rate for the Blofeld because it's the ``true'' underlying behavior, but I've since changed the patch editor back to displaying time because using something other than what's on the Blofeld screen would really confuse owners, and in the Blofeld's case it's a subtle difference.} One consequence of this is that the Decay and Release together are as long as the Attack, because if you're basing on rate, then the amount of time to go up is the same as the total amount of time to go {\it down}, and both Decay and Release go down. Thus we have a {\it max width} of 1/3 for all four portions: but at any time they can only sum to 1/3 [attack] + 1/3 [sustain] + 1/3 [decay + release]. + +In the Blofeld ADSR, all the values go 0...127, and the angles are displayed by Edisyn to go from vertical to \(\pi/4\) (we don't want them too flattened out). See if the code below makes sense now: + +\begin{verbatim} +String[] timeKeys = new String[] { null, "attack", "decay", null, "release" }; +String[] heightKeys = new String[] { null, null, "sustain", "sustain", null }; +double[] timeConstants = new double[] { 0, 0.3333, 0.3333, 0.3333, 0.3333}; +double[] heightConstants = new double[] { 0, 1.0, 1.0 / 127.0, 1.0/127.0, 0 }; +double[] angles = new double[] { 0, (Math.PI/4/127), (Math.PI/4/127), 0, (Math.PI/4/127) }; +JComponent comp = new EnvelopeDisplay(this, Color.red, "ADSR", + timeKeys, heightKeys, timeConstants, heightConstants, angles); +\end{verbatim} + +Sometimes you need {\it both} angles {\it and} times. For example, in the E-Mu Ultra Proteus, attack and decay and release are measured in rate, but ``hold'' measures how long (in time) we stay at maximum attack before starting decay. To do this, if you set the appropriate angle to {\tt EnvelopeDisplay.TIME}, then the corresponding time constant will revert to being used as a measure of time rather than a maximum length for the angle. + +This {\it still} might not be flexible enough for you. For example, the Yamaha TX81Z has, shall we say, an unusual approach to defining angles. You can do further post processing on the \(\langle x,y\rangle\) coordinates of each of the points (where both X and Y vary from 0...1) by overriding the {\bf postProcess(...)} method like this: + +\begin{verbatim} +JComponent comp = new EnvelopeDisplay(this, Color.red, "ADSR", + timeKeys, heightKeys, timeConstants, heightConstants, angles) + { + public void postProcess(double[] xVals, double[] yVals) + { + // modify xVals and yVals as you see fit. + } + }; +\end{verbatim} + +Envelopes generally stretch to fill all available space: they're particularly good to put as the ``last'' element in an HBox via addLast(). But you might want to add them elsewhere and fix them to a specific width. In this case, just call {\bf setPreferredWidth(...)}. + + +\item{\bf Spacers}\quad Occasionally you might need to add some fixed space to separate widgets. See the {\tt Strut} class for factory methods that can build some struts for you. + +\end{itemize} + +\paragraph{Dynamically Changing Widgets} + +One gotcha which shows up in a number of synthesizers (particularly in effects sections) is that if you change (say) the effect type, the number of available parameters, and their names, will change as well. Eventually Edisyn will have a widget that assists in this, but for now you'll have to manually add and remove widgets. + +Edisyn's patch editors usually do this by defining a bunch of HBoxes, one for each effect type, and then remove the current HBox and add the correct new one dynamically in response to the user changing types. You can see a simple example of this in the Waldorf Microwave XT code, and a more elaborate version in the Blofeld code (where different effects actually share specific widgets). + +You'll have to manually remove and add these widgets or HBoxes. But when should you do so? That's pretty easy: when the effect type has been updated. Typically the effect type is shown as a Chooser, and when it is updated, the Chooser's {\bf update(...)} method is called: + +\begin{verbatim} +JComponent comp = new Chooser("Effect Type", this, "effecttype", types) + { + public void update(String key, Model model) + { + super.update(key, model); // be sure to do this first + int newValue = model.get(key, 0); // 0 is the default if the key doesn't exist, but it will. + + // now do something according to the value newValue + } + }; +\end{verbatim} + +You'll see various patch editors have implemented update(...) for various purposes. + +Hand in hand with this: in some cases you want the update(...) method to be called not only when the widget's key is updated, but when {\it some other key} is updated. To do this, you can {\it register} a widget to be updated for that key as well. This is done as follows: + +\begin{verbatim} +model.register("keyname", widget); +\end{verbatim} + +For example, in the Yamaha TX81Z, the operator frequency is computed as a combination of three widgets: and in the final widget (``Fine'') the final frequency is displayed. To do this, we have registered the ``Fine'' widget to revise itself (via the map(...) method) whenever any of three different parameters is updated. + +\subsection{Step Five: Get Input from the Synth (and File Loading) Working} +\label{filereading} + +There are two ways the synth can send you information: as a bulk sysex patch dump and as individual parameters. We'll start with the bulk sysex patch dump. + +\paragraph{Bulk Dumps} + +First, you need to implement the {\bf recognize(...)} method. This method tells Edisyn that you recognize a bulk dump sysex message. You should verify the message length and the header to determine that it's a bulk dump and in fact meant is for your type of synthesizer {\it and} is probably correct. This method will also be called when loading a sysex file from disk.\footnote{In fact the primary purpose of this method is to recognize sysex data loaded from disk: and so other sysex messages don't have their own recognize(...) method.} + +Next, you need to implement the {\bf parse(...)} method. In this method you will be given a data array and your job is to set the model parameters according to your parsing of this array. You set parameters using the {\bf set(...)} methods in the model, like this: + +\begin{verbatim} +getModel().set(numericalKey, 4.2); // or whatever new value +getModel().set(stringKey, "newValue"); // or whatever new value +\end{verbatim} + +It is possible that the parse(...) method will actually contain multiple sysex messages, if you loaded from a file. For example, the Yamaha TX81Z's patch isn't a single sysex messages, it's {\it two} messages, to be backward compatible with an unimportant earlier synthesizer for some ridiculous Yamaha reason. When you receive a dump via the synth, it'll only be one or other other of these messages. But if you receive a TX81Z dump from a file, it'll be both messages. Thankfully, the parse() method will tell you whether you're receiving from a file or not.\footnote{Though in fact the TX81Z implementation\,---\,and in fact all Edisyn's parse editors to date\,---\,don't change their parse(...) behavior when receiving from a file.} So if you do something fancy with emit(...) later, you may need to revise your parse(...) implementation. + +You also need to implement the {\bf gatherPatchInfo(...)} method. This method is nontrivial to implement. Its function is to work with the user to determine the patch number, bank number, etc. necessary to ask the synthesizer for a given patch. I suggest you take a look at existing patch editors to see how they have implemented it, and largely copy that. You'll notice that patch-gathering code usually pops up a dialog box with a bunch of rows in it. How is this done? Edisyn's Synth.java class has a special method to make this easy: {\bf showMultiOption(...)}. + +Additionally, you need to override methods which issue a dump request to the synth: + +\begin{itemize} +\item {\bf performRequestDump(...)} or {\bf requestDump(...)}\quad Override {\it one} of these methods to request a dump from the synth of a specific patch. requestDump(...) is simpler: you just return bytes corresponding to a sysex message to broadcast to the synth. performRequestDump(...) lets you manually issue the proper commands. + +In the second case, the edisyn.Midi class, instantiated in the {\bf midi} instance variable, has several methods for constructing MIDI messages: you can send them, or send sysex messages (as byte arrays) via the {\bf tryToSendMIDI()} or {\bf tryToSendSysex()} methods. Also you'll have to handle changing the patch: see the information in Blank.java's documentation on this method for an example. + +Both of these methods take a Model called {\bf tempModel} which will hold information concerning the patch number and bank number that you should fetch. This model was built by gatherPatchInfo(...). + +\item {\bf performRequestCurrentDump(...)} or {\bf requestCurrentDump(...)}\quad Override {\it one} of these methods to request a dump from the synth of the current patch being played. These methods are basically just like performRequestDump(...) and requestDump(...), but they don't take a model (there's no patch number). +\end{itemize} + +You will also probably need to implement {\bf changePatch(...)} to issue a patch change (it'll be called as part of performRequestDump(...)). It's possible that your synthesizer must pause for a bit after a patch change (the Blofeld, for example, requires almost 200ms). You may want to implement the {\bf getPauseAfterChangePatch()} method to slow Edisyn down. If your synth can't change patches to whatever you're editing, that's okay, but you'll need to handle the right behavior later on when you emit a patch to it. + + +If your synth cannot load the current patch you can avoid implementing some of these methods by saying the following: + +\begin{verbatim} +receiveCurrent.setEnabled(false); // turns of the "Request Current Patch" menu option +\end{verbatim} + +You should do this in an overridden version of the {\bf sprout()} method (be absolutely sure to call super.sprout() first). + +You will also want to override some other methods. First {\bf getPatchName(model)} should extract the patch name from the provided model (probably via {\tt model.get("name", "foo")}\ ). Second, you also will want to override the {\bf revisePatchName(...)} method if you've not already done so for the StringComponent widget. This method modifies a provided name and returns a corrected version. The default version, which you might call first (via super), removes trailing whitespace. You can then revise incorrect characters, length, and so on. Third, if your synthesizer uses an ID to distinguish itself from other synthesizers of the same type (the Waldorf synths do this for example), you should override the {\bf reviseID(...)} method to correct provided IDs. If this method returns {\tt null} (the default), the ID won't even appear as an option. + +Finally, you will probably want to override the {\bf revise()} method to verify that all the model parameters have valid values, and tweak them if not. The default version, which you can call via super, does most of the heavy lifting: it bounds the values to between their min and max. You might also verify that the patch name is correct here. See the Waldorf Blofeld code as an example of what to do. + +See also the description of these methods in {\tt edisyn/synth/Blank.java} + +\paragraph{Individual Parameters} + +[If your synth doesn't send out individual parameters, or you don't want to be bothered right now in handling this, you can just ignore this section for now]. Individual parameters might come in as sysex messages, as CC messages, or as NRPN. Here are your options: + +\begin{itemize} +\item {\bf Sysex Messages}\quad Here, override the method {\bf parseParameter(...)}. Note that the provided data might be something else sent via sysex besides just a parameter change. You can test for that too (and handle it here if you like). +\item {\bf NRPN or Cooked CC messages}\quad A cooked CC message is one which doesn't violate any of the RPN/NRPN rules (it's not 6, 38, 97, 98, 99, 100, or 101). At present Edisyn does not recognize 14-bit CC. If your messages are always cooked or are NRPN, then you can handle them via {\bf handleSynthCCOrNRPN(...)}, which takes a special {\bf MIDI.CCData} argument that tells you about the message (see the Midi.java class). +\item {\bf Raw CC Messages}\quad A raw CC message is any message number 0...127 just sent out willy-nilly, not respecting things like RPN/NRPN or 14-bit CC. If your synth sends out raw CC messages, you need to override {\bf getExpectsRawCCFromSynth()} to return {\bf true}. Then you handle the messages via {\bf handleSynthCCOrNRPN(...)} as discussed above. +\end{itemize} + +Again, you update one or more parameters in response to these messages using one of: + +\begin{verbatim} +getModel().set(numericalKey, 4.2); // or whatever new value +getModel().set(stringKey, "newValue"); // or whatever new value +\end{verbatim} + +\paragraph{Note on File Loading} + +If your bulk dumps come in as sysex messages, then congratulations, you already have file loading working. If not, you will need to {\it invent} a bulk sysex format and implement it in the {\bf parse(...)} (and later {\bf emit(...)} methods even if your synthesizer never sends stuff via sysex (such as is the case in the PreenFM2). That way you can still load and save files. + +You probably ought to use the ``educational use'' wildcard MIDI sysex ID (0x7D). Edisyn's made-up sysex header for the PreenFM2 currently looks like this: {\tt 0xF0, 0x7D, P, R, E, E, N, F, M, 2, {\it version}.} Presently {\it version} is {\tt 0x0}. You might do something similar. + +\subsection{Step Six: Get Output to the Synth (and File Writing) Working} + +If you've gotten this far, writing is simpler than parsing and requesting, because you've already written a lot of the support code. You can write out both bulk dumps and individual parameters (as you tweak widgets). + +\paragraph{Bulk Dumps} + +You will need to implement {\it one} of the following two methods: either {\bf emitAll(Model, ...)} or {\bf emit(Model, ...)}. The emit(Model, ...) method is simpler: you just build data for a sysex message and return it. In emitAll(Model, ...), you build an array consisting of {\it either} javax.sound.midi.SimpleMessage objects {\it or} byte[] arrays corresponding to sysex messages, or a mixture of the two. These will be emitted one by one. Most commonly you just override emit(Model, ...). + +Both emit(Model, ...) and emitAll(Model, ...) receive a temporary model. This model will contain a small bit of data sufficient to inform you of the patch and bank number are that the patch is going to be emitted to (via Edisyn's ``write'' procedure). Alternatively if the {\it toWorkingMemory} argument is TRUE, then you're supposed to emit to current working memory (Edisyn's ``send'' procedure). + +You may not be able to write, or you may not be able to send to a specific patch, or to the current patch, depending on your synthesizer. If so, you can do any of: + +\begin{verbatim} +transmitTo.setEnabled(false); // turns of the "Send to Patch..." menu option +transmitCurrent.setEnabled(false); // turns of the "Send to Current Patch" menu option +writeTo.setEnabled(false); // turns of the "Write to Patch..." menu option +\end{verbatim} + +Again, these should be set in an overridden version of the {\bf sprout()} method. Be sure to call {\bf super.sprout()} first, or bad things will probably happen. + +Note that emit(Model, ...) and emitAll(Model, ...) are also used to write out files. If you implemented emitAll(...), be aware that Edisyn will strip out all of the javax.sound.midi.SimpleMessage messages and just pack together then remaining sysex messages. This is what will result in multiple sysex messages being read in in a single parse(...) dump, as discussed earlier. + +Some synthesizers need a bit of time to rest after receiving a dump before they can do anything else. You can tell Edisyn to pause after a dump by overriding {\bf getPauseAfterSendAllParameters()}. + +\paragraph{Individual Parameters} + +In response to changing a widget, Edisyn will try to change a parameter on your synthesizer. This is similar to the bulk dump. Specifically, there are two methods, {\bf emit(String)} and {\bf emitAll(String)}, which work like their bulk counterparts, except that they are tasked to emit a {\it single parameter} to the synthesizer. Implement only {\it one} of these methods. If you don't want to do this, just don't implement these methods. + +If your synthesizer accepts NRPN (such as the PreenFM2), the Midi.java file has some utility methods for building NRPN messages easily. + +It's possible that your synthesizer can only accept messages at a certain rate. You may want to implement the {\bf getPauseBetweenMIDISends()} method to slow Edisyn down. + +\paragraph{Bulk Dumps Via Individual Parameters} + +Some synthesizers, such as the PreenFM2, do not accept a bulk dump method at all. Rather you send a ``bulk dump'' as a whole lot of individual parameter changes. If your synthesizer is of this type, you should override the method {\bf getSendsAllParametersInBulk()} method to return {\bf false}. + +\paragraph{Note on File Writing} + +See the earlier note at the end of Section \ref{filereading} about File Loading: as discussed there, if your synth doesn't read or write sysex, you'll still need to {\it invent} a bulk sysex format, and implement it in the {\bf emit(...)} and {\bf parse(...)} methods, so you can save and load files to disk. + + +\subsection{Step Seven: Create an Init File} + +Now that you've got everything coded and working (hah!) it's time to create an Init file. To do this, either request an init patch from the synthesizer, or create an appropriate one yourself. Then save it out as a sysex file. + +Next, move that file and rename it to {\tt edisyn/synth/yamahadx7/YamahaDX7.init}. Edisyn will load this file to initialize your patch editor. To do this, add to the very bottom of your constructor the following line: + +\begin{verbatim} +loadDefaults(); +\end{verbatim} + +\subsection{Step Eight: Get Batch Downloads Working} + +Edisyn can download many patches at once. To support this, you need to implement a few methods.\footnote{Until you implement {\bf getPatchLocationName(...)} to return something other than {\tt null}, Edisyn will keep the Batch Downloads menu disabled. So when you implement this method, be sure to also implement the other methods here at the same time.} First, there's + {\bf getPatchLocationName(...)}, which returns as a String a short version of the patch address (bank, name) to be used in a saved filename. Next, there's {\bf getNextPatchLocation(...)} which, given a Model containing a patch address, returns a model with the ``next'' patch address (wrapping around to the very first address if necessary). Finally you need to implement {\bf patchLocationEquals(...)}, which compares two patches to see if they contain the same patch address. + +A few synthesizers (notably the PreenFM2) don't send individual patches as single sysex patch dumps, but rather send them as multiple separate NRPN or CC messages. Edisyn needs to know this so it can make a better guess at whether a patch dump has arrived and is ready to be saved. To let Edisyn know that your patch editor is for a synthesizer of this type, override the method {\bf getReceivesPatchesInBulk()} to return false. + +Compared to the other stuff, this step is easy.\footnote{Note that lots of synthesizers have sysex facilities to dump the entire patch memory, or dump an entire bank, etc. Edisyn doesn't use these; it requests patches one by one. This is slower but saves you a lot of coding and is consistent across synthesizers. So you're welcome.} + +\subsection{Step Nine: Other Stuff} +You're almost done! Some other items you might want to do. First, you may need to tweak the mutability of parameters. No string parameters are mutable, but by default all numerical ones are (including checkboxes and choosers). Occasionally you'd want to make some of those immutable so they will not be modified during merge, hill-climbing, etc. To do this, you can call {\bf setStatus(..., Model.STATUS\_IMMUTABLE)} on the model. + +Second, whenever your patch editor becomes the front window, the method {\bf windowBecameFront()} will be called. You could override this to send a special message to your synth to update it somehow. For example, the Waldorf Microwave XT patch editors send a message to the Microwave XT to tell it to switch from single to multi-mode (or back) as appropriate. + +Finally when the user clicks on the close box, the method {\bf requestCloseWindow()} is called. You can override this to query the user about saving the patch etc. first, and then finally return the appropriate value to inform Edisyn that the window should in fact be closed. Though in fact currently no patch editors implement this method at all. + +\subsection{Step Ten: Submit Your Patch Editor!} + +\begin{itemize} +\item Clean up the editor code, make it really polished, well documented, and good looking. +\item Test it well. +\item Copyright your editor code at the top of the file. License the editor code under Apache 2.0 (I don't accept anything else). +\item Send the whole directory to me! I'd love to include it. +\end{itemize} + + +\end{document} diff --git a/docs/manual/HillClimbPanel.png b/docs/manual/HillClimbPanel.png new file mode 100644 index 00000000..1eff0d19 Binary files /dev/null and b/docs/manual/HillClimbPanel.png differ diff --git a/docs/manual/HillClimbing.graffle b/docs/manual/HillClimbing.graffle new file mode 100644 index 00000000..749d1483 --- /dev/null +++ b/docs/manual/HillClimbing.graffle @@ -0,0 +1,5950 @@ + + + + + ApplicationVersion + + com.omnigroup.OmniGraffle7 + 186.9.0.304876 + + ColorProfiles + + + data + + AAAMSExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNz + cE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA + 0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAABs + d3RwdAAAAfAAAAAUYmtwdAAAAgQAAAAUclhZWgAAAhgAAAAUZ1hZ + WgAAAiwAAAAUYlhZWgAAAkAAAAAUZG1uZAAAAlQAAABwZG1kZAAA + AsQAAACIdnVlZAAAA0wAAACGdmlldwAAA9QAAAAkbHVtaQAAA/gA + AAAUbWVhcwAABAwAAAAkdGVjaAAABDAAAAAMclRSQwAABDwAAAgM + Z1RSQwAABDwAAAgMYlRSQwAABDwAAAgMdGV4dAAAAABDb3B5cmln + aHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRl + c2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAS + c1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADz + UQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAA + b6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAA + ACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5p + ZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0 + IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVD + IDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBz + UkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJl + ZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0y + LjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRp + b24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAA + AAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAA + AAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAA + AAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8A + VABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCy + ALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkB + HwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGh + AakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkEC + SwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAML + AxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD + +QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+ + BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicG + NwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeG + B5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJ + EAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAqu + CsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUM + jgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/ + DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQ + uRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMD + EyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgV + mxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhA + GGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQb + OxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5A + HmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUh + oSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJ + JTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo + 1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyi + LNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw + 2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUT + NU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85 + vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5g + PqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpD + fUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iR + SNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxO + JU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1Oq + U/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZ + uFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+z + YAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedm + PWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yv + bQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11z + uHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6Rnql + ewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2C + MIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZ + if6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+R + qJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQ + mfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobai + JqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqP + qwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCsziz + rrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8Ibyb + vRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjG + RsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+4 + 0DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ + 8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr + 5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7iju + tO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4 + +cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf// + + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + CreationDate + 2017-08-30 15:12:03 +0000 + Creator + Sean Luke + GraphDocumentVersion + 14 + GuidesLocked + NO + GuidesVisible + YES + ImageCounter + 1 + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2018-03-06 18:32:57 +0000 + Modifier + Sean Luke + MovementHandleVisible + NO + NotesVisible + NO + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {611.99997711181641, 792} + + NSPrintReverseOrientation + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + ReadOnly + NO + Sheets + + + ActiveLayerIndex + 0 + AutoAdjust + 6 + AutosizingMargin + 72 + BackgroundGraphic + + Bounds + {{0, 0}, {1728, 1466}} + Class + GraffleShapes.CanvasBackgroundGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasDimensionsOrigin + {0, 0} + CanvasOrigin + {0, 0} + CanvasSizingMode + 1 + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 in = 1.00000 in + GraphicsList + + + Class + Group + Graphics + + + Bounds + {{1147.679974347353, 488.77664439604609}, {52, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 224 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr\partightenfactor0 + +\f0\fs24 \cf0 (A - Z) - Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{1207.429974347353, 498.77664439604609}, {8, 8}} + Class + ShapedGraphic + ID + 225 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 225 + + ID + 226 + Points + + {1208.4126083655522, 581.78324650995364} + {1211.2582395357233, 507.27338630182919} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 228 + + + + Bounds + {{1170.4908717832504, 572.29664252922862}, {26, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 227 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr\partightenfactor0 + +\f0\fs24 \cf0 A - Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{1204.2408717832504, 582.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 228 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 228 + + ID + 229 + Points + + {1205.1648480070853, 665.78333213081009} + {1208.0668667130512, 590.77664345612197} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 234 + + + + Class + LineGraphic + Head + + ID + 234 + + ID + 230 + Points + + {1199.3938903042981, 834.78257436439435} + {1204.8378531841461, 674.77740232993256} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 233 + + + + Bounds + {{1158.2408717832504, 832.27998834848404}, {8, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 231 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{1187.4908717832504, 663.27998834848404}, {9, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 232 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{1195.2408717832504, 835.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 233 + Shape + Circle + Style + + fill + + Color + + b + 0.9098039269 + g + 0.40392178299999998 + r + 0.88235312700000001 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{1200.9908717832504, 666.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 234 + Shape + Circle + Style + + fill + + Color + + b + 0.1717065871 + g + 0.20398768780000001 + r + 1.0108638999999999 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + ID + 235 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + + Class + Group + Graphics + + + Class + LineGraphic + Head + + ID + 287 + + ID + 236 + Points + + {383.26579455306529, 837.84438441235636} + {530.73420544693465, 788.21559228461172} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 286 + + + + Class + LineGraphic + Head + + ID + 238 + + ID + 237 + Points + + {534.81893576349171, 782.28361040353059} + {529.18106423650829, 642.27804288081177} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 287 + + + + Bounds + {{525, 633.78166493585832}, {8, 8}} + Class + ShapedGraphic + ID + 238 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 238 + + ID + 239 + Points + + {527.12408817238895, 569.77828861991759} + {528.87591182761105, 633.28336466442477} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 288 + + + + Bounds + {{615.69456308211704, 739.13298657589951}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 240 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 C - Z} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 242 + + ID + 241 + Points + + {539.29333835048828, 785.42919804458018} + {615.4012247336957, 761.48377688639016} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 287 + + + + Bounds + {{615.69456308211704, 756.13298657589951}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 242 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{586.6434533847173, 451.43998990952969}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 243 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B - Z} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 245 + + ID + 244 + Points + + {529.22348318339414, 561.36652601076173} + {578.49650392392402, 474.64308579138424} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 288 + + + + Bounds + {{576.71998710930347, 466.72962345479118}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 245 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{530.46173313770691, 468.9227225416256}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 246 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B - C} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 248 + + ID + 247 + Points + + {526.85633153934793, 560.78226764951512} + {524.68193532198586, 492.71007678588654} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 288 + + + + Bounds + {{520.53826686229309, 484.2123560868871}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 248 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{546, 863.99998068809509}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 249 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 C - B} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 251 + + ID + 250 + Points + + {535.23638104525378, 791.27381179225665} + {539.84015267468828, 878.79579078982704} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 287 + + + + Bounds + {{536.07653372458628, 879.28961423335659}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 251 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{614.63998693227768, 829.27998834848404}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 252 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 C - A} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 254 + + ID + 253 + Points + + {538.58447710508278, 789.50134573393325} + {615.05550982931186, 847.55863096025291} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 287 + + + + Bounds + {{614.63998693227768, 846.27998834848404}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 254 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{279.46624341386092, 599.0399866104126}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 255 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A - C} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 257 + + ID + 256 + Points + + {381.14418662800773, 667.58689921673522} + {317.07205677790739, 619.73307575277545} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 289 + + + + Bounds + {{309.46624341386092, 613.0399866104126}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 257 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{567.35998731851578, 495.35998892784119}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 258 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B - A} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 260 + + ID + 259 + Points + + {530.6901671804892, 562.70342150360091} + {597.66982013321808, 515.93655576585297} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 288 + + + + Bounds + {{597.35998731851578, 509.35998892784119}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 260 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{264.46624341386092, 708.27998834848404}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 261 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A - B} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 263 + + ID + 262 + Points + + {380.97463026549099, 672.73028390227228} + {302.24161314772397, 723.82969279370275} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 289 + + + + Bounds + {{294.46624341386092, 722.27998834848404}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 263 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{350.25, 572.27998834848404}, {26, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 264 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr\partightenfactor0 + +\f0\fs24 \cf0 A - Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{407, 716.27998834848404}, {30, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 265 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A + C} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{403.5, 617.27998834848404}, {30, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 266 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A + B} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{541, 631.27998834848404}, {31, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 267 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B + C} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{463, 661.27998834848404}, {60, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 268 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A + (B + C)} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 271 + + ID + 269 + Points + + {531.43887683780702, 784.02821135951672} + {439.5611231621927, 713.03176533745159} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 287 + + + + Class + LineGraphic + Head + + ID + 271 + + ID + 270 + Points + + {388.2977411762738, 673.04895707144362} + {432.45225882362672, 707.51101962565156} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 289 + + + + Bounds + {{432, 706.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 271 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 274 + + ID + 272 + Points + + {523.41839784501678, 568.0051204229278} + {438.58160215498361, 632.55485627404073} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 288 + + + + Class + LineGraphic + Head + + ID + 274 + + ID + 273 + Points + + {388.44312922465048, 667.70765953532009} + {431.30687077551227, 637.85231716188105} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 289 + + + + Bounds + {{431, 631.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 274 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 278 + + ID + 275 + Points + + {532.39726483112941, 783.10827266382728} + {453.60273516887054, 671.9517040331408} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 287 + + + + Class + LineGraphic + Head + + ID + 278 + + ID + 276 + Points + + {524.32788454417789, 568.9014079794008} + {453.67211545582222, 664.65856871756739} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 288 + + + + Class + LineGraphic + Head + + ID + 278 + + ID + 277 + Points + + {389.24796417327161, 670.14420075081159} + {446.50203582673328, 668.41577594632133} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 289 + + + + Bounds + {{447, 664.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 278 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{384, 582.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 279 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 279 + + ID + 280 + Points + + {384.92397776802721, 665.78333219022738} + {387.82602223196159, 590.77664450674013} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 289 + + + + Class + LineGraphic + Head + + ID + 289 + + ID + 281 + Points + + {379.1530185231249, 834.78257436446472} + {384.5969814768751, 674.77740233250336} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 286 + + + + Bounds + {{338, 832.27998834848404}, {8, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 282 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{534.95998804271221, 763.91998292505741}, {14, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 283 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 C} + VerticalPad + 0.0 + + + + Bounds + {{536, 558.27998834848404}, {9, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 284 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{367.25, 663.27998834848404}, {9, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 285 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{375, 835.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 286 + Shape + Circle + Style + + fill + + Color + + b + 0.9098039269 + g + 0.40392178299999998 + r + 0.88235312700000001 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{531, 782.77998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 287 + Shape + Circle + Style + + fill + + Color + + b + 0.1137529686 + g + 0.80187815429999998 + r + 0.01483638026 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{523, 561.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 288 + Shape + Circle + Style + + fill + + Color + + b + 1.0013448 + g + 0.35146147010000001 + r + 0.18423730129999999 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{380.75, 666.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 289 + Shape + Circle + Style + + fill + + Color + + b + 0.1717065871 + g + 0.20398768780000001 + r + 1.0108638999999999 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 288 + + ID + 290 + Points + + {381.13911634008025, 835.31973242157869} + {524.86088365991986, 569.24024427538939} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 101 + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 286 + + + + ID + 291 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + + Class + Group + Graphics + + + Class + LineGraphic + Head + + ID + 221 + + ID + 195 + Points + + {813.13911236763909, 835.31973549415716} + {956.86126155857085, 569.24045376728156} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 220 + + + + Bounds + {{799.24999034404755, 524.15998828411102}, {81, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 196 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 (A - Z) + (B - Z)} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 199 + + ID + 197 + Points + + {1008.8248287982329, 472.98499591659549} + {892.05519290883672, 540.59705503886278} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 202 + + + + Class + LineGraphic + Head + + ID + 199 + + ID + 198 + Points + + {823.79586602878703, 583.86152004203416} + {884.3644950413601, 545.27139702485249} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 214 + + + + Bounds + {{884.15998023748398, 538.85231708352148}, {8, 8}} + Class + ShapedGraphic + ID + 199 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{1018.6434437287649, 451.43998990952969}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 200 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B - Z} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 202 + + ID + 201 + Points + + {961.22347950023561, 561.36652940836132} + {1010.4966326010385, 474.64316447818914} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 221 + + + + Bounds + {{1008.719977453351, 466.72962345479118}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 202 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{1043.5199760049582, 502.55998876690865}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 203 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B - A} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 205 + + ID + 204 + Points + + {962.49600601959548, 562.44613737272232} + {1027.9442292708277, 509.39416949408741} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 221 + + + + Bounds + {{1027.4399770349264, 502.55998876690865}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 205 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{694.7999844700098, 712.79998406767845}, {29.999999999999996, 13.999999999999998}} + Class + ShapedGraphic + ID + 206 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A - B} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 208 + + ID + 207 + Points + + {813.04230119181977, 672.83133244670285} + {732.50784980541425, 728.24889546558154} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 222 + + + + Bounds + {{724.7999844700098, 726.79998406767845}, {7.9999999999999973, 8}} + Class + ShapedGraphic + ID + 208 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{782.24999034404755, 572.27998834848404}, {26, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 209 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr\partightenfactor0 + +\f0\fs24 \cf0 A - Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{835.49999034404755, 617.27998834848404}, {30, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 210 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A + B} + VerticalPad + 0.0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 213 + + ID + 211 + Points + + {955.41839370616924, 568.00512765710675} + {870.58172869421196, 632.55503486651014} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 221 + + + + Class + LineGraphic + Head + + ID + 213 + + ID + 212 + Points + + {820.44319099235406, 667.7077618554049} + {863.30776175533151, 637.85360687426385} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 222 + + + + Bounds + {{862.99999034404755, 631.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 213 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{815.99999034404755, 582.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 214 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707219999998 + g + 0.0039215674619999998 + r + 0.02352940291 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 214 + + ID + 215 + Points + + {816.92396957118592, 665.78333224637151} + {819.82603837389752, 590.77664549932058} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 222 + + + + Class + LineGraphic + Head + + ID + 222 + + ID + 216 + Points + + {811.15301186394584, 834.78257446586611} + {816.59708143440162, 674.77740604011308} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 220 + + + + Bounds + {{769.99999034404755, 832.27998834848404}, {8, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 217 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{967.99999034404755, 558.27998834848404}, {9, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 218 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{799.24999034404755, 663.27998834848404}, {9, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 219 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{806.99999034404755, 835.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 220 + Shape + Circle + Style + + fill + + Color + + b + 0.9098039269 + g + 0.40392178299999998 + r + 0.88235312700000001 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{954.99999034404755, 561.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 221 + Shape + Circle + Style + + fill + + Color + + b + 1.0013448 + g + 0.35146147010000001 + r + 0.18423730129999999 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{812.74999034404755, 666.27998834848404}, {8, 8}} + Class + ShapedGraphic + ID + 222 + Shape + Circle + Style + + fill + + Color + + b + 0.1717065871 + g + 0.20398768780000001 + r + 1.0108638999999999 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + ID + 223 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + + Bounds + {{50.25, 97}, {125, 28}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 3 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr\partightenfactor0 + +\f0\fs24 \cf0 Even Further Beyond A\ +From Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{123.25, 148}, {52, 28}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 4 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr\partightenfactor0 + +\f0\fs24 \cf0 Beyond A\ +From Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{206, 292}, {30, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 5 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A + C} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{202.5, 193}, {30, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 6 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A + B} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{340, 207}, {31, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 7 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B + C} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{262, 237}, {60, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 8 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A + (B + C)} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{185, 107}, {8, 8}} + Class + ShapedGraphic + ID + 9 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707220137119 + g + 0.0039215674623847008 + r + 0.023529402911663055 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 9 + + ID + 10 + Points + + {187.17633579867186, 157.50343519018216} + {188.82366333812831, 115.49656477615257} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 24 + + + + Class + LineGraphic + Head + + ID + 14 + + ID + 11 + Points + + {330.43887683811602, 359.74822301063392} + {238.56112317047206, 288.7517769782778} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 34 + + + + Class + LineGraphic + Head + + ID + 14 + + ID + 12 + Points + + {187.29774116960397, 248.76896873148564} + {231.45225873394529, 283.23103139180751} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 36 + + + + Class + LineGraphic + Head + + ID + 15 + + ID + 13 + Points + + {333.81832914703074, 358.00364638954301} + {328.18167085225957, 218.4963536104855} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 34 + + + + Bounds + {{231, 282}, {8, 8}} + Class + ShapedGraphic + ID + 14 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707220137119 + g + 0.0039215674623847008 + r + 0.023529402911663055 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{324, 210}, {8, 8}} + Class + ShapedGraphic + ID + 15 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707220137119 + g + 0.0039215674623847008 + r + 0.023529402911663055 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 15 + + ID + 16 + Points + + {326.12324174078452, 145.49832341262578} + {327.87675831519942, 209.50167658584888} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 35 + + + + Class + LineGraphic + Head + + ID + 19 + + ID + 17 + Points + + {322.41839784503082, 143.72513207446198} + {237.58160215532746, 208.27486792600774} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 35 + + + + Class + LineGraphic + Head + + ID + 19 + + ID + 18 + Points + + {187.4431292152023, 243.42767117330121} + {230.30687065640529, 213.57232864277179} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 36 + + + + Bounds + {{230, 207}, {8, 8}} + Class + ShapedGraphic + ID + 19 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707220137119 + g + 0.0039215674623847008 + r + 0.023529402911663055 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 23 + + ID + 20 + Points + + {331.39726483073315, 358.8282843156249} + {252.60273515647583, 247.67171569346272} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 34 + + + + Class + LineGraphic + Head + + ID + 23 + + ID + 21 + Points + + {323.3278845432236, 144.62141963021088} + {252.67211542963199, 240.37858034971364} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 35 + + + + Class + LineGraphic + Head + + ID + 23 + + ID + 22 + Points + + {188.24796417306527, 245.86421239545396} + {245.50203582390023, 244.13578750347062} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 36 + + + + Bounds + {{246, 240}, {8, 8}} + Class + ShapedGraphic + ID + 23 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707220137119 + g + 0.0039215674623847008 + r + 0.023529402911663055 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{183, 158}, {8, 8}} + Class + ShapedGraphic + ID + 24 + Shape + Circle + Style + + fill + + Color + + b + 0.0039215707220137119 + g + 0.0039215674623847008 + r + 0.023529402911663055 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Class + LineGraphic + Head + + ID + 24 + + ID + 25 + Points + + {183.92397776952791, 241.50334384180115} + {186.82602225849553, 166.49665615927714} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 36 + + + + Class + LineGraphic + Head + + ID + 36 + + ID + 26 + Points + + {178.15301852309895, 410.50258601597977} + {183.59698147592528, 250.49741398398723} + + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 33 + + + + Bounds + {{45, 279}, {75, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 27 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 Current Patch} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{127, 282}, {8, 8}} + Class + ShapedGraphic + FontInfo + + Color + + b + 0.37722 + g + 0.950882 + r + 1 + + + ID + 28 + Shape + Circle + Style + + fill + + Color + + b + 0.44971162080764771 + g + 0.95043057203292847 + r + 1.0005350112915039 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{137, 408}, {8, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 29 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 Z} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{345, 355.5}, {9, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 30 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 C} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{335, 134}, {9, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 31 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 B} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{166.25, 239}, {9, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 32 + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0.0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs24 \cf0 A} + VerticalPad + 0.0 + + Wrap + NO + + + Bounds + {{174, 411}, {8, 8}} + Class + ShapedGraphic + ID + 33 + Shape + Circle + Style + + fill + + Color + + b + 0.90980392694473267 + g + 0.40392178297042847 + r + 0.88235312700271606 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{330, 358.5}, {8, 8}} + Class + ShapedGraphic + ID + 34 + Shape + Circle + Style + + fill + + Color + + b + 0.11375296860933304 + g + 0.80187815427780151 + r + 0.014836380258202553 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{322, 137}, {8, 8}} + Class + ShapedGraphic + ID + 35 + Shape + Circle + Style + + fill + + Color + + b + 1.0013447999954224 + g + 0.35146147012710571 + r + 0.18423730134963989 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + Bounds + {{179.75, 242}, {8, 8}} + Class + ShapedGraphic + ID + 36 + Shape + Circle + Style + + fill + + Color + + b + 0.17170658707618713 + g + 0.20398768782615662 + r + 1.0108639001846313 + space + 9eaea0911d89d63e39e95f2e2116eaec7e0bb91e + + + shadow + + Draws + NO + + + Text + + VerticalPad + 0.0 + + + + GridInfo + + HPages + 3 + KeepToScale + + Layers + + + Artboards + + Lock + + Name + Layer 1 + Print + + View + + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoLineLength + 0.20000000298023224 + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 1 + UniqueID + 1 + VPages + 2 + VisibleVoidKey + 1 + + + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UseEntirePage + + WindowInfo + + CurrentSheet + 0 + Frame + {{-146, 0}, {1141, 877}} + ShowInfo + + ShowRuler + + Sidebar + + SidebarWidth + 216 + Sidebar_Tab + 0 + VisibleRegion + {{1185.5493853352391, 289.54735740449888}, {758.97435897435867, 902.56410256410254}} + ZoomValues + + + Canvas 1 + 0.78000000000000003 + 1 + + + + compressOnDiskKey + + copyLinkedImagesKey + + createSinglePDFKey + + exportAreaKey + 3 + exportQualityKey + 100 + exportSizesKey + + 1 + + + fileFormatKey + 0 + htmlImageTypeKey + 0 + includeBackgroundGraphicKey + + includeNonPrintingLayersKey + + lastExportTypeKey + 8 + marginWidthKey + 0.0 + previewTypeKey + 0 + readOnlyKey + + resolutionForBMPKey + 1 + resolutionForGIFKey + 1 + resolutionForHTMLKey + 1 + resolutionForJPGKey + 1 + resolutionForPNGKey + 1 + resolutionForTIFFKey + 1 + resolutionUnitsKey + 0 + saveAsFlatFileOptionKey + 3 + useArtboardsKey + + useMarginKey + + useNotesKey + + + diff --git a/docs/manual/HillClimbing.pdf b/docs/manual/HillClimbing.pdf new file mode 100644 index 00000000..bd970a28 Binary files /dev/null and b/docs/manual/HillClimbing.pdf differ diff --git a/docs/manual/HillClimbingBig.pdf b/docs/manual/HillClimbingBig.pdf new file mode 100644 index 00000000..6e75cfb9 Binary files /dev/null and b/docs/manual/HillClimbingBig.pdf differ diff --git a/edisyn/CCMap.java b/edisyn/CCMap.java new file mode 100644 index 00000000..7c940ab0 --- /dev/null +++ b/edisyn/CCMap.java @@ -0,0 +1,188 @@ +/*** + Copyright 2017 by Sean Luke + Licensed under the Apache License version 2.0 +*/ + +package edisyn; + +import java.util.*; +import java.io.*; +import java.util.prefs.*; + +/** + @author Sean Luke +*/ + +public class CCMap + { + HashMap map = new HashMap(); + HashMap reverseMap = new HashMap(); + HashMap typeMap = new HashMap(); + + Preferences keyPrefs; + Preferences typePrefs; + + public static final int NRPN_OFFSET = 256; + + public static final int TYPE_ABSOLUTE_CC = 0; + public static final int TYPE_RELATIVE_CC_64 = 1; + public static final int TYPE_RELATIVE_CC_0 = 2; + public static final int TYPE_NRPN = 3; + + public Integer munge(int cc, int pane) + { + return Integer.valueOf((cc << 8) | pane); + } + + public int cc(Integer munge) + { + if (munge == null) return -1; + else return munge.intValue() >>> 8; + } + + public int pane(Integer munge) + { + if (munge == null) return -1; + else return munge & 255; + } + + public int getTypeForCCPane(int cc, int pane) + { + Integer val = (Integer)(typeMap.get(munge(cc, pane))); + if (val == null) return -1; + else return (val.intValue()); + } + + public String getKeyForCCPane(int cc, int pane) + { + return getKeyForInteger(munge(cc, pane)); + } + + /** Returns the model key for the given CC value, or null if there is none. */ + public String getKeyForInteger(Integer munge) + { + return (String)map.get(munge); + } + + /** Returns the model key for the given CC value, or null if there is none. */ + public Integer getIntegerForKey(String key) + { + return (Integer)reverseMap.get(key); + } + + public int getCCForKey(String key) + { + return cc(getIntegerForKey(key)); + } + + public int getPaneForKey(String key) + { + return pane(getIntegerForKey(key)); + } + + public void setKeyForCCPane(int cc, int pane, String key) + { + setKeyForInteger(munge(cc, pane), key); + } + + public void setTypeForCCPane(int cc, int pane, int type) + { + typeMap.put(munge(cc, pane), Integer.valueOf(type)); + typePrefs.put("" + munge(cc, pane).intValue(), "" + type); + try + { + typePrefs.sync(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + /** Sets the model key for the given CC value, and syncs the Preferences (which isn't cheap). */ + public void setKeyForInteger(Integer munge, String key) + { + map.put(munge, key); + reverseMap.put(key, munge); + + keyPrefs.put("" + munge.intValue(), key); + try + { + keyPrefs.sync(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + + public CCMap(Preferences keyPrefs, Preferences typePrefs) + { + this.keyPrefs = keyPrefs; + this.typePrefs = typePrefs; + + // do a load + try + { + String[] keys = keyPrefs.keys(); + for(int i = 0; i < keys.length; i++) + { + // each Key holds a CC INTEGER + + int munge = 0; + try { munge = Integer.parseInt(keys[i]); } + catch (Exception e) { e.printStackTrace(); } + + // each Value holds a MODEL KEY STRING + + map.put(Integer.valueOf(munge), keyPrefs.get(keys[i], "-")); + reverseMap.put(keyPrefs.get(keys[i], "-"), Integer.valueOf(munge)); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + + try + { + String[] keys = typePrefs.keys(); + for(int i = 0; i < keys.length; i++) + { + // each Key holds a CC INTEGER + + int munge = 0; + try { munge = Integer.parseInt(keys[i]); } + catch (Exception e) { e.printStackTrace(); } + + // each Value holds a TYPE INTEGER + + int type = 0; + try { type = Integer.parseInt(typePrefs.get(keys[i], "0")); } + catch (Exception e) { e.printStackTrace(); } + + typeMap.put(Integer.valueOf(munge), Integer.valueOf(type)); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + public void clear() + { + try + { + keyPrefs.clear(); + typePrefs.clear(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + map = new HashMap(); + reverseMap = new HashMap(); + } + } diff --git a/edisyn/Edisyn.java b/edisyn/Edisyn.java index c338b415..7efbeee3 100644 --- a/edisyn/Edisyn.java +++ b/edisyn/Edisyn.java @@ -8,7 +8,8 @@ import edisyn.gui.*; import edisyn.synth.*; import javax.swing.*; - +//import com.apple.eawt.*; +//import com.apple.eawt.event.*; /**** Top-level launcher class. For the moment, run as @@ -19,18 +20,26 @@ public class Edisyn { + public static final int VERSION = 17; + public static void main(String[] args) { -/* - //MicrowaveXT blofeld = new MicrowaveXT(); - Blofeld blofeld = new Blofeld(); - blofeld.sprout(); - JFrame frame = ((JFrame)(SwingUtilities.getRoot(blofeld))); - frame.setVisible(true); - blofeld.setupMIDI("Choose MIDI devices to send to and receive from."); -*/ - if (Synth.doNewSynthPanel() == null) - System.exit(0); - } + try { + System.setProperty("apple.awt.graphics.EnableQ2DX", "true"); + System.setProperty("apple.laf.useScreenMenuBar", "true"); + + // This no longer works as of Java 7 + //System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Edisyn"); + // This DOES work, but it's not necessary as the menu says "Edisyn" anyway + // System.setProperty("apple.awt.application.name", "Edisyn"); + + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch(Exception e) { } + + + if (Synth.doNewSynthPanel() == null) + System.exit(0); + } } diff --git a/edisyn/HillClimb.java b/edisyn/HillClimb.java new file mode 100644 index 00000000..0aa01fe2 --- /dev/null +++ b/edisyn/HillClimb.java @@ -0,0 +1,1652 @@ +/*** + Copyright 2017 by Sean Luke + Licensed under the Apache License version 2.0 +*/ + +package edisyn; + +import java.util.*; +import java.io.*; +import edisyn.gui.*; +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.border.*; +import java.awt.*; +import java.awt.event.*; +import edisyn.synth.*; + +/*** + + Procedure: + + Menu -> Start Hill-Climb [or Menu -> Reset Hill-Climb if already started] + Creates Hill-Climb Panel and Resets it to current patch + Menu -> Stop Hill-Climb + Deletes Hill-Climb Panel + + Each sound has: + 1. Done (or Keep?) -> Deletes Hill-Climb Panel and sets current patch to this sound [probably requires double-check dialog panel] + 2. Try -> Sends sound to current patch + 3. Best / Second Best / Third Best + + The Panel also has: + 1. Iterate + 2. Rate + 3. Backup + + + Hill-Climbing variations + + + +***/ + + +public class HillClimb extends SynthPanel + { + // OPERATIONS + public static final int OPERATION_SEED_FROM_PATCH = 0; + public static final int OPERATION_SEED_FROM_NUDGE = 1; + public static final int OPERATION_SEED_FROM_FOUR = 2; + public static final int OPERATION_SEED_FROM_SIX = 3; + public static final int OPERATION_CLIMB = 4; + public static final int OPERATION_CONSTRICT = 5; + + /// HILL CLIMBING STACK + + class State + { + Model[] parents; + int[] parentIndices; + boolean[] parentsSelected; + Model[] children; + int operation; + } + ArrayList stack = new ArrayList(); + + + /// NUMBER OF CANDIDATE SOLUTIONS + + public static final int NUM_CANDIDATES = 32; + public static final int STAGE_SIZE = 16; + public static final int ARCHIVE_SIZE = 6; + // There are more models than candidates: #17 is the current Model + public static final int NUM_MODELS = NUM_CANDIDATES + ARCHIVE_SIZE + 1; + + // models currently being played and displayed + Model[] currentModels = new Model[NUM_MODELS]; + + public int operation; + + JRadioButton[][] ratings = new JRadioButton[NUM_MODELS + 1][3]; + JCheckBox[] selected = new JCheckBox[NUM_CANDIDATES]; + PushButton[] plays = new PushButton[NUM_MODELS]; + public static final int INITIAL_MUTATION_RATE = 37; // roughly 5 when we do weight^3 + public static final int STANDARD_RECOMBINATION_RATE = 75; + Blank blank; + Category iterations; + int currentPlay = 0; + int temporaryPlay = -1; + HBox nudgeBox; + PushButton retry; + PushButton climb; + PushButton reset; + PushButton back; + PushButton constrict; + JCheckBox bigger; + LabelledDial mutationRate; + LabelledDial recombinationRate; + + JComboBox method = new JComboBox(new String[] { "Hill-Climber", "Constrictor", "Smooth Constrictor" }); + + VBox candidates; + VBox extraCandidates1; + VBox extraCandidates2; + + State popStack() + { + if (stack.size() == 0) + return null; + else + return (State)(stack.remove(stack.size() - 1)); + } + + void pushStack(int[] parentIndices, Model[] parents, boolean[] parentsSelected, Model[] children) + { + State state = new State(); + state.parents = new Model[parents.length]; + state.parentIndices = new int[parents.length]; + state.parentsSelected = new boolean[parentsSelected.length]; + state.operation = operation; + + for(int i = 0; i < parents.length; i++) + { + state.parents[i] = copy(parents[i]); + state.parentIndices[i] = parentIndices[i]; + state.parentsSelected[i] = parentsSelected[i]; + } + + for(int i = 0; i < parentsSelected.length; i++) + { + state.parentsSelected[i] = parentsSelected[i]; + } + + state.children = new Model[children.length]; + for(int i = 0; i < children.length; i++) + { + state.children[i] = copy(children[i]); + } + + stack.add(state); + } + + State topStack() + { + if (stack.size() == 0) + return null; + else + return (State)(stack.get(stack.size() - 1)); + } + + boolean stackEmpty() + { + return (stack.size() == 0); + } + + boolean stackInitial() + { + return (stack.size() == 1); + } + + String titleForButton(int _i) + { + return "Play " + (_i < 16 ? + (char)('a' + _i) : + (_i < NUM_CANDIDATES ? + (char)('A' + (_i - 16)) : + (_i < NUM_MODELS - 1 ? + (char)('q' + (_i - NUM_CANDIDATES)) : + 'z'))); + } + + /* + void seed(int type) + { + Random random = synth.random; + String[] keys = synth.getMutationKeys(); + double weight = blank.getModel().get("recombinationrate", 0) / 100.0; + + switch(val) + { + case 0: + { + for(int i = 0; i < 4; i++) + currentModels[i] = (Model)(synth.nudge[i].clone()); + int m = 4; + for(int i = 0; i < 4; i++) + for(int j = 0; j < 4; j++) + { + if (j == i) continue; + currentModels[m++] = currentModels[i].copy().crossover(random, currentModels[j], keys, weight); + } + // fill the next 16 + for(int i = 16; i < 32; i++) + { + // pick two parents, try to make them different from one another + int p1 = random.nextInt(16); + int p2 = 0; + for(int j = 0; j < 100; j++) + { + p2 = random.nextInt(16); + if (p2 != p1) break; + } + currentModels[i] = currentModels[p1].copy().crossover(random, currentModels[p1], keys, weight); + } + operation = OPERATION_SEED_FROM_NUDGE; + } + break; + case 1: + { + int m = 4; + for(int i = 0; i < 4; i++) + for(int j = 0; j < 4; j++) + { + if (j == i) continue; + currentModels[m++] = currentModels[i].copy().crossover(random, currentModels[j], keys, weight); + } + // fill the next 16 + for(int i = 16; i < 32; i++) + { + // pick two parents, try to make them different from one another + int p1 = random.nextInt(16); + int p2 = 0; + for(int j = 0; j < 100; j++) + { + p2 = random.nextInt(16); + if (p2 != p1) break; + } + currentModels[i] = currentModels[p1].copy().crossover(random, currentModels[p1], keys, weight); + } + operation = OPERATION_SEED_FROM_FOUR; + } + break; + case 2: + { + int m = 6; + for(int i = 0; i < 6; i++) + for(int j = 0; j < 6; j++) + { + if (j == i) continue; + if (m >= 32) break; + currentModels[m++] = currentModels[i].copy().crossover(random, currentModels[j], keys, weight); + } + operation = OPERATION_SEED_FROM_SIX; + } + break; + } + } + */ + + VBox buildCandidate(int i) + { + final int _i = i; + + VBox vbox = new VBox(); + plays[_i] = new PushButton(titleForButton(i)) + { + public void perform() + { + if (synth.isSendingTestNotes()) + { + temporaryPlay = i; + } + else + { + for(int j = 0; j < NUM_MODELS; j++) + { + plays[j].getButton().setForeground(new JButton().getForeground()); + plays[j].getButton().setText(titleForButton(j)); + } + plays[_i].getButton().setForeground(Color.RED); + plays[_i].getButton().setText("" + titleForButton(i) + ""); + + // change the model, send all parameters, maybe play a note, + // and then restore the model. + Model backup = synth.model; + synth.model = currentModels[_i]; + synth.sendAllParameters(); + synth.doSendTestNote(false); + synth.model = backup; + temporaryPlay = i; + } + + } + }; + plays[i].getButton().setFocusable(false); + vbox.add(plays[i]); + + +/* + Box b = new Box(BoxLayout.X_AXIS); + b.setBackground(Style.BACKGROUND_COLOR()); + b.add(Box.createGlue()); + b.add(ratings[i][0] = new JRadioButton("1")); + ratings[i][0].setFocusable(false); + ratings[i][0].setForeground(Style.TEXT_COLOR()); + ratings[i][0].setFont(Style.SMALL_FONT()); + ratings[i][0].putClientProperty("JComponent.sizeVariant", "small"); + ratings[i][0].setHorizontalTextPosition(SwingConstants.CENTER); + ratings[i][0].setVerticalTextPosition(JRadioButton.TOP); + + b.add(ratings[i][1] = new JRadioButton("2")); + ratings[i][1].setFocusable(false); + ratings[i][1].setForeground(Style.TEXT_COLOR()); + ratings[i][1].setFont(Style.SMALL_FONT()); + ratings[i][1].putClientProperty("JComponent.sizeVariant", "small"); + ratings[i][1].setHorizontalTextPosition(SwingConstants.CENTER); + ratings[i][1].setVerticalTextPosition(JRadioButton.TOP); + + b.add(ratings[i][2] = new JRadioButton("3")); + ratings[i][2].setFocusable(false); + ratings[i][2].setForeground(Style.TEXT_COLOR()); + ratings[i][2].setFont(Style.SMALL_FONT()); + ratings[i][2].putClientProperty("JComponent.sizeVariant", "small"); + ratings[i][2].setHorizontalTextPosition(SwingConstants.CENTER); + ratings[i][2].setVerticalTextPosition(JRadioButton.TOP); + b.add(Box.createGlue()); + vbox.add(b); +*/ + + HBox hh = new HBox(); + VBox vv = new VBox(); + + Box b = new Box(BoxLayout.X_AXIS); + b.setBackground(Style.BACKGROUND_COLOR()); + b.add(Box.createGlue()); + b.add(ratings[i][0] = new JRadioButton("1")); + ratings[i][0].setFocusable(false); + ratings[i][0].setForeground(Style.TEXT_COLOR()); + ratings[i][0].setFont(Style.SMALL_FONT()); + ratings[i][0].putClientProperty("JComponent.sizeVariant", "small"); + ratings[i][0].setOpaque(false); // for windows + + vv.add(b); + + b = new Box(BoxLayout.X_AXIS); + b.setBackground(Style.BACKGROUND_COLOR()); + b.add(Box.createGlue()); + b.add(ratings[i][1] = new JRadioButton("2")); + ratings[i][1].setFocusable(false); + ratings[i][1].setForeground(Style.TEXT_COLOR()); + ratings[i][1].setFont(Style.SMALL_FONT()); + ratings[i][1].putClientProperty("JComponent.sizeVariant", "small"); + ratings[i][1].setOpaque(false); // for windows + b.add(Box.createGlue()); + vv.add(b); + + b = new Box(BoxLayout.X_AXIS); + b.setBackground(Style.BACKGROUND_COLOR()); + b.add(Box.createGlue()); + b.add(ratings[i][2] = new JRadioButton("3")); + ratings[i][2].setFocusable(false); + ratings[i][2].setForeground(Style.TEXT_COLOR()); + ratings[i][2].setFont(Style.SMALL_FONT()); + ratings[i][2].putClientProperty("JComponent.sizeVariant", "small"); + ratings[i][2].setOpaque(false); // for windows + b.add(Box.createGlue()); + vv.add(b); + + hh.add(vv); + + vv = new VBox(); + if (i < NUM_CANDIDATES) + { + selected[i] = new JCheckBox(""); + selected[i].setFocusable(false); + selected[i].setForeground(Style.TEXT_COLOR()); + selected[i].setOpaque(false); // for windows + selected[i].setFont(Style.SMALL_FONT()); + selected[i].setSelected(true); + selected[i].putClientProperty("JComponent.sizeVariant", "small"); + vv.add(selected[i]); + } + hh.add(vv); + vbox.add(hh); + + + JMenuItem[] doItems = new JMenuItem[13]; + doItems[0] = new JMenuItem("Keep Patch"); + doItems[0].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + // Keep for sure? + if (synth.showSimpleConfirm("Keep Patch", "Load Patch into Editor?")) + { + synth.tabs.setSelectedIndex(0); + synth.setSendMIDI(false); + // push to undo if they're not the same + if (!currentModels[_i].keyEquals(synth.getModel())) + synth.undo.push(synth.getModel()); + + // Load into the current model + currentModels[_i].copyValuesTo(synth.getModel()); + synth.setSendMIDI(true); + synth.sendAllParameters(); + } + } + }); + if (_i == NUM_CANDIDATES + ARCHIVE_SIZE) + doItems[0].setEnabled(false); + + doItems[1] = new JMenuItem("Edit Patch"); + doItems[1].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + Synth newSynth = synth.doDuplicateSynth(); + // Copy the parameters forward into the synth, then + // link the synth's model back to currentModels[_i]. + // We do this because the new synth's widgets are registered + // with its model, so we can't just replace the model. + // But we can certainly replace currentModels[_i]! + newSynth.setSendMIDI(false); + currentModels[_i].copyValuesTo(newSynth.getModel()); + newSynth.setSendMIDI(true); + currentModels[_i] = newSynth.getModel(); + newSynth.sendAllParameters(); + } + }); + if (_i == NUM_CANDIDATES + ARCHIVE_SIZE) + doItems[1].setEnabled(false); + + doItems[2] = new JMenuItem("Save to File"); + doItems[2].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + Model backup = synth.model; + synth.model = currentModels[_i]; + synth.doSaveAs("" + stack.size() + "." + + (_i < NUM_CANDIDATES ? (_i + 1) : ("A" + (_i - NUM_CANDIDATES + 1))) + + "." + synth.getPatchName(synth.getModel()) + ".syx"); + synth.model = backup; + synth.updateTitle(); + } + }); + if (_i == NUM_CANDIDATES + ARCHIVE_SIZE) + doItems[2].setEnabled(false); + + doItems[3] = new JMenuItem("Load from File"); + doItems[3].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + Model backup = synth.model; + synth.model = currentModels[_i]; + synth.doOpen(false); + currentModels[_i] = synth.model; + synth.model = backup; + synth.updateTitle(); + } + }); + if (_i == NUM_CANDIDATES + ARCHIVE_SIZE) + doItems[3].setEnabled(false); + + doItems[4] = null; + + doItems[5] = new JMenuItem("Nudge Candidates to Me"); + doItems[5].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + Random random = synth.random; + String[] keys = synth.getMutationKeys(); + + for(int i = 0; i < NUM_CANDIDATES; i++) + { + if (i == _i) continue; + currentModels[i].recombine(random, currentModels[_i], keys, synth.nudgeRecombinationWeight).mutate(random, keys, synth.nudgeMutationWeight); + } + } + }); + + doItems[6] = null; + + doItems[7] = new JMenuItem("Archive to q"); + doItems[7].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + currentModels[NUM_CANDIDATES + 0] = (Model)(currentModels[_i].clone()); + } + }); + + doItems[8] = new JMenuItem("Archive to r"); + doItems[8].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + currentModels[NUM_CANDIDATES + 1] = (Model)(currentModels[_i].clone()); + } + }); + + doItems[9] = new JMenuItem("Archive to s"); + doItems[9].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + currentModels[NUM_CANDIDATES + 2] = (Model)(currentModels[_i].clone()); + } + }); + + doItems[10] = new JMenuItem("Archive to t"); + doItems[10].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + currentModels[NUM_CANDIDATES + 3] = (Model)(currentModels[_i].clone()); + } + }); + + doItems[11] = new JMenuItem("Archive to u"); + doItems[11].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + currentModels[NUM_CANDIDATES + 4] = (Model)(currentModels[_i].clone()); + } + }); + + doItems[12] = new JMenuItem("Archive to v"); + doItems[12].addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + currentModels[NUM_CANDIDATES + 5] = (Model)(currentModels[_i].clone()); + } + }); + + PushButton options = new PushButton("Options", doItems); + options.getButton().setFocusable(false); + vbox.add(options); + + return vbox; + } + + + public HillClimb(Synth synth) + { + super(synth); + + blank = new Blank(); + + addAncestorListener ( new AncestorListener () + { + public void ancestorAdded ( AncestorEvent event ) + { + requestFocusInWindow(); + } + + public void ancestorRemoved ( AncestorEvent event ) + { + // will get removed + } + + public void ancestorMoved ( AncestorEvent event ) + { + // don't care + } + } ); + setFocusable(true); + + addKeyListener(new KeyListener() + { + public void keyPressed(KeyEvent e) + { + } + public void keyReleased(KeyEvent e) + { + } + public void keyTyped(KeyEvent e) + { + char c = e.getKeyChar(); + + if (c >= 'a' && c <= 'p') + { + int p = (int)(c - 'a'); + plays[p].perform(); + } + else if (c >= 'A' && c <= 'P' && NUM_CANDIDATES == 32) + { + int p = (int)(c - 'A' + 16); + plays[p].perform(); + } + else if ((c >= 'q' && c <= 'v')) + { + int p = (int)(c - 'q' + NUM_CANDIDATES); + plays[p].perform(); + } + else if (c =='z') + { + int p = (int)(NUM_MODELS - 1); + plays[p].perform(); + } + else if (c == ' ') + { + climb.perform(); + } + else if (c == KeyEvent.VK_BACK_SPACE) + { + back.perform(); + } + else if (c == KeyEvent.VK_ENTER) + { + retry.perform(); + } + else if (c >= '1' && c <= '3') + { + ratings[lastPlayedSound()][(int)(c - '1')].setSelected(true); + } + } + }); + + ButtonGroup one = new ButtonGroup(); + ButtonGroup two = new ButtonGroup(); + ButtonGroup three = new ButtonGroup(); + + + VBox top = new VBox(); + HBox toprow = new HBox(); + add(top, BorderLayout.CENTER); + + // add globals + + Category panel = new Category(null, "Iteration 1", Style.COLOR_GLOBAL()); + iterations = panel; + + + HBox iterationsBox = new HBox(); + + VBox vbox = new VBox(); + +// has to be first so others can have their size based on it + back = new PushButton("Back Up") + { + public void perform() + { + pop(); + resetCurrentPlay(); + } + }; + back.getButton().setFocusable(false); + + + climb = new PushButton("Climb") + { + public void perform() + { + climb(true); + resetCurrentPlay(); + } + }; + climb.getButton().setPreferredSize(back.getButton().getPreferredSize()); + climb.getButton().setFocusable(false); + + vbox.add(climb); + + String s = synth.getLastX("HillClimbMutationRate", null); + mutationRate = new LabelledDial("Rate", blank, "mutationrate", Style.COLOR_GLOBAL(), 0, 100) + { + public String map(int val) + { + double v = ((val / 100.0) * (val / 100.0) * (val / 100.0)) * 100; + if (v == 100) + return "100.0"; + else if (v >= 10.0) + return String.format("%.2f", v); + else + return String.format("%.3f", v); + } + + public void update(String key, Model model) + { + super.update(key, model); + synth.setLastX("" + model.get(key), "HillClimbMutationRate", null); + } + }; + + int v = INITIAL_MUTATION_RATE; + if (s != null) + try { v = Integer.parseInt(s); } catch (Exception e) { e.printStackTrace(); } + if (v < 0 || v > 100) v = INITIAL_MUTATION_RATE; + mutationRate.setState(v); + + blank.getModel().set("mutationrate", v); + vbox.add(mutationRate); + + iterationsBox.add(vbox); + + vbox = new VBox(); + constrict = new PushButton("Constrict") + { + public void perform() + { + constrict(); + resetCurrentPlay(); + } + }; + constrict.getButton().setPreferredSize(back.getButton().getPreferredSize()); + constrict.getButton().setFocusable(false); + vbox = new VBox(); + vbox.add(constrict); + + s = synth.getLastX("HillClimbRecombinationRate", null); + + recombinationRate = new LabelledDial("Rate", blank, "recombinationrate", Style.COLOR_GLOBAL(), 0, 100) + { + public String map(int val) + { + if (val == 100) + return "100.0"; + else if (val >= 10.0) + return String.format("%.2f", (double)val); + else + return String.format("%.3f", (double)val); + } + + public void update(String key, Model model) + { + super.update(key, model); + synth.setLastX("" + model.get(key), "HillClimbRecombinationRate", null); + } + }; + + v = STANDARD_RECOMBINATION_RATE; + if (s != null) + try { v = Integer.parseInt(s); } catch (Exception e) { e.printStackTrace(); } + if (v < 0 || v > 100) v = STANDARD_RECOMBINATION_RATE; + recombinationRate.setState(v); + + blank.getModel().set("recombinationrate", v); + vbox.add(recombinationRate); + + iterationsBox.add(vbox); + + + vbox = new VBox(); + + retry = new PushButton("Retry") + { + public void perform() + { + again(); + resetCurrentPlay(); + } + }; + retry.getButton().setPreferredSize(back.getButton().getPreferredSize()); + retry.getButton().setFocusable(false); + vbox.add(retry); + + // add the aforementioned Back up button + vbox.add(back); + + /* + reset = new PushButton("Reset") + { + public void perform() + { + if (synth.showSimpleConfirm("Reset", "Are you sure you want to reset the Hill-Climber?")) + { + initialize((Model)(synth.getModel().clone()), OPERATION_SEED_FROM_PATCH); + resetCurrentPlay(); + } + } + }; + reset.getButton().setPreferredSize(back.getButton().getPreferredSize()); + reset.getButton().setFocusable(false); + vbox.add(reset); + */ + + reset = new PushButton("Reset...", + new String[] { "From Original Patch", + "From Nudge Targets", + "From First Four Candidates", + "From First Six Candidates" }) + { + public void perform(int val) + { + initialize(val == OPERATION_SEED_FROM_PATCH ? synth.getModel() : null, val); + resetCurrentPlay(); + } + }; + reset.getButton().setPreferredSize(back.getButton().getPreferredSize()); + reset.getButton().setFocusable(false); + vbox.add(reset); + + iterationsBox.add(vbox); + + panel.add(iterationsBox, BorderLayout.CENTER); + + s = synth.getLastX("HillClimbMethod", null); + method.setFont(Style.SMALL_FONT()); + method.putClientProperty("JComponent.sizeVariant", "small"); + method.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + int m = method.getSelectedIndex(); + synth.setLastX("" + m, "HillClimbMethod", null); + setMethod(m); + } + }); + + v = 0; + if (s != null) + try { v = Integer.parseInt(s); } catch (Exception e) { e.printStackTrace(); } + if (v < 0 || v > 2) v = 0; + method.setSelectedIndex(v); + + + JLabel methodLabel = new JLabel("Method: "); + methodLabel.setForeground(Style.TEXT_COLOR()); + methodLabel.setFont(Style.SMALL_FONT()); + methodLabel.putClientProperty("JComponent.sizeVariant", "small"); + methodLabel.setOpaque(false); // for windows + + HBox eb = new HBox(); + eb.add(methodLabel); + eb.add(method); // we do addLast rather than add to overcome the stupid OS X "Smoo..." bug. + + + bigger = new JCheckBox("Big"); + bigger.setFocusable(false); + bigger.setOpaque(false); // for windows + bigger.setForeground(Style.TEXT_COLOR()); + bigger.setFont(Style.SMALL_FONT()); + bigger.putClientProperty("JComponent.sizeVariant", "small"); + + s = synth.getLastX("HillClimbBigger", null); + + bigger.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + candidates.remove(extraCandidates1); + candidates.remove(extraCandidates2); + if (bigger.isSelected()) + { + candidates.add(extraCandidates1); + candidates.add(extraCandidates2); + } + candidates.revalidate(); + candidates.repaint(); + + synth.setLastX("" + bigger.isSelected(), "HillClimbBigger", null); + } + }); + + boolean bb = false; + if (s != null) + try { bb = (s.equals("true")); } catch (Exception e) { e.printStackTrace(); } + + bigger.setSelected(bb); + eb.addLast(bigger); + + panel.add(eb, BorderLayout.NORTH); + + toprow.add(panel); + + panel = new Category(null, "Archive", Style.COLOR_A()); + HBox hbox = new HBox(); + panel.add(hbox); + for(int i = 0; i < ARCHIVE_SIZE; i++) + { + vbox = buildCandidate(NUM_CANDIDATES + i); + hbox.add(vbox); + } + panel.add(hbox); + toprow.addLast(panel); + top.add(toprow); + + // Add Candidates + + panel = new Category(null, "Candidates", Style.COLOR_B()); + + hbox = new HBox(); + + candidates = new VBox(); + for(int i = 0; i < NUM_CANDIDATES; i++) + { + vbox = buildCandidate(i); + hbox.add(vbox); + + if (i % 8 == 7) + { + VBox vv = new VBox(); + if (i != 7) + vv.add(Strut.makeVerticalStrut(20)); + vv.add(hbox); + hbox = new HBox(); + + if (i == 23) + extraCandidates1 = vv; + else if (i == 31) + extraCandidates2 = vv; + else + candidates.add(vv); + } + } + + panel.add(candidates, BorderLayout.WEST); + + HBox hb = new HBox(); + hb.add(panel); + + VBox currentAndNone = new VBox(); + + // Add Current + panel = new Category(null, "Current", Style.COLOR_C()); + + vbox = buildCandidate(NUM_MODELS - 1); + HBox currentHBox = new HBox(); + currentHBox.add(vbox); + panel.add(currentHBox); + currentAndNone.add(panel); + + // Add None + panel = new Category(null, "None", Style.COLOR_C()); + + vbox = new VBox(); + Box b = new Box(BoxLayout.X_AXIS); + b.setBackground(Style.BACKGROUND_COLOR()); + b.add(Box.createGlue()); + b.add(ratings[NUM_MODELS][0] = new JRadioButton("1")); + ratings[NUM_MODELS][0].setFocusable(false); + ratings[NUM_MODELS][0].setForeground(Style.TEXT_COLOR()); + ratings[NUM_MODELS][0].setFont(Style.SMALL_FONT()); + ratings[NUM_MODELS][0].putClientProperty("JComponent.sizeVariant", "small"); + ratings[NUM_MODELS][0].setOpaque(false); // for windows + b.add(Box.createGlue()); + vbox.add(b); + + b = new Box(BoxLayout.X_AXIS); + b.setBackground(Style.BACKGROUND_COLOR()); + b.add(Box.createGlue()); + b.add(ratings[NUM_MODELS][1] = new JRadioButton("2")); + ratings[NUM_MODELS][1].setFocusable(false); + ratings[NUM_MODELS][1].setForeground(Style.TEXT_COLOR()); + ratings[NUM_MODELS][1].setFont(Style.SMALL_FONT()); + ratings[NUM_MODELS][1].putClientProperty("JComponent.sizeVariant", "small"); + ratings[NUM_MODELS][1].setOpaque(false); // for windows + b.add(Box.createGlue()); + vbox.add(b); + + b = new Box(BoxLayout.X_AXIS); + b.setBackground(Style.BACKGROUND_COLOR()); + b.add(Box.createGlue()); + b.add(ratings[NUM_MODELS][2] = new JRadioButton("3")); + ratings[NUM_MODELS][2].setFocusable(false); + ratings[NUM_MODELS][2].setForeground(Style.TEXT_COLOR()); + ratings[NUM_MODELS][2].setFont(Style.SMALL_FONT()); + ratings[NUM_MODELS][2].putClientProperty("JComponent.sizeVariant", "small"); + ratings[NUM_MODELS][2].setOpaque(false); // for windows + b.add(Box.createGlue()); + vbox.add(b); + VBox bar = new VBox(); + bar.addBottom(vbox); + HBox foo = new HBox(); + foo.add(bar); + foo.add(Strut.makeHorizontalStrut(40)); + panel.add(foo); + + currentAndNone.add(panel); + + hb.addLast(currentAndNone); + + top.add(hb); + + for(int i = 0; i < ratings.length; i++) + { + one.add(ratings[i][0]); + two.add(ratings[i][1]); + three.add(ratings[i][2]); + } + + for(int i = NUM_CANDIDATES; i < NUM_CANDIDATES + ARCHIVE_SIZE; i++) + { + currentModels[i] = (Model)(synth.getModel().clone()); + } + currentModels[NUM_CANDIDATES + ARCHIVE_SIZE] = synth.getModel(); + + setMethod(method.getSelectedIndex()); + } + + + + public static final int UPDATE_SOUND_RATE = 1; + int updateSoundTick = 0; + Model backup = null; + + boolean isShowingPane() + { + return (synth.hillClimbPane != null && synth.tabs.getSelectedComponent() == synth.hillClimbPane); + } + + public void updateSound() + { + updateSoundTick++; + if (updateSoundTick >= UPDATE_SOUND_RATE) + updateSoundTick = 0; + + if (updateSoundTick == 0) + { + if (isShowingPane()) + { + for(int i = 0; i < NUM_MODELS; i++) + { + plays[i].getButton().setForeground(new JButton().getForeground()); + plays[i].getButton().setText(titleForButton(i)); + } + if (temporaryPlay >= 0) + { + plays[temporaryPlay].getButton().setForeground(Color.RED); + plays[temporaryPlay].getButton().setText("" + titleForButton(temporaryPlay) + ""); + backup = synth.model; + synth.model = currentModels[temporaryPlay]; + synth.sendAllParameters(); + temporaryPlay = -1; + } + else + { + currentPlay++; + if (currentPlay >= NUM_CANDIDATES || + currentPlay >= 16 && !bigger.isSelected()) + currentPlay = 0; + plays[currentPlay].getButton().setForeground(Color.RED); + plays[currentPlay].getButton().setText("" + titleForButton(currentPlay) + ""); + + // change the model, send all parameters, maybe play a note, + // and then restore the model. + backup = synth.model; + synth.model = currentModels[currentPlay]; + synth.sendAllParameters(); + } + } + } + } + + void setMethod(int method) + { + boolean c = (method == 0); + climb.getButton().setEnabled(c); + constrict.getButton().setEnabled(!c); + for(int i = 0; i < ratings.length; i++) + for(int j = 0; j < ratings[i].length; j++) + if (ratings[i][j] != null) ratings[i][j].setEnabled(c); + for(int i = 0; i < selected.length; i++) + if (selected[i] != null) selected[i].setEnabled(!c); + mutationRate.setEnabled(c); + recombinationRate.setEnabled(!c); + this.method.setSelectedIndex(method); + } + + int lastPlayedSound() + { + if (temporaryPlay >=0) + return temporaryPlay; + else return currentPlay; + } + + public void postUpdateSound() + { + repaint(); + if (backup!= null) + synth.model = backup; + backup = null; + } + + boolean startedUp = false; + + public void startup() + { + if (!startedUp) + { + resetCurrentPlay(); + if (!synth.isSendingTestNotes()) + { + synth.doSendTestNotes(); + } + } + startedUp = true; + } + + public void shutdown() + { + if (startedUp) + { + synth.doSendAllSoundsOff(false); + if (synth.isSendingTestNotes()) + { + synth.doSendTestNotes(); + } + // restore patch + synth.sendAllParameters(); + } + startedUp = false; + } + + public void resetCurrentPlay() + { + currentPlay = NUM_CANDIDATES - 1; + temporaryPlay = -1; + } + + Model copy(Model model) + { + if (model != null) + return model.copy(); + else return null; + } + + void again() + { + if (stackEmpty()) + { + // uh oh... + System.err.println("Warning (HillClimb): " + "Empty Stack"); + return; + } + else if (operation == OPERATION_SEED_FROM_PATCH) + { + initialize(synth.getModel(), operation); + } + else if (operation == OPERATION_SEED_FROM_NUDGE || operation == OPERATION_SEED_FROM_FOUR || operation == OPERATION_SEED_FROM_SIX) + { + initialize(null, operation); + } + else if (operation == OPERATION_CLIMB) + { + State state = popStack(); + System.arraycopy(state.children, 0, currentModels, 0, state.children.length); + + ratings[NUM_MODELS][0].setSelected(true); + ratings[NUM_MODELS][1].setSelected(true); + ratings[NUM_MODELS][2].setSelected(true); + + for(int j = 0; j < state.parentIndices.length; j++) + { + if (state.parentIndices[j] != -1) + ratings[state.parentIndices[j]][j].setSelected(true); + } + + for(int j = 0; j < state.parentsSelected.length; j++) + { + selected[j].setSelected(state.parentsSelected[j]); + } + + climb(false); + } + else if (operation == OPERATION_CONSTRICT) + { + State state = popStack(); + System.arraycopy(state.children, 0, currentModels, 0, state.children.length); + + ratings[NUM_MODELS][0].setSelected(true); + ratings[NUM_MODELS][1].setSelected(true); + ratings[NUM_MODELS][2].setSelected(true); + constrict(); + } + } + + void pop() + { + if (stackEmpty()) + { + // uh oh... + System.err.println("Warning (HillClimb) 2: " + "Empty Stack"); + return; + } + else if (stackInitial()) + { + // do nothing + } + else + { + State state = popStack(); + operation = state.operation; + System.arraycopy(state.children, 0, currentModels, 0, state.children.length); + + for(int j = 0; j < state.parentIndices.length; j++) + { + if (state.parentIndices[j] != -1) + ratings[state.parentIndices[j]][j].setSelected(true); + } + + for(int j = 0; j < state.parentsSelected.length; j++) + { + selected[j].setSelected(state.parentsSelected[j]); + } + + iterations.setName("Iteration " + stack.size()); + repaint(); + } + } + + public void startHillClimbing() + { + for(int i = NUM_CANDIDATES; i < NUM_CANDIDATES + ARCHIVE_SIZE; i++) + { + currentModels[i] = (Model)(synth.getModel().clone()); + } + currentModels[NUM_CANDIDATES + ARCHIVE_SIZE] = synth.getModel(); + + initialize(synth.getModel(), OPERATION_SEED_FROM_PATCH); + } + + boolean[] getSelectedResults() + { + boolean[] sel = new boolean[NUM_CANDIDATES]; + for(int i = 0; i < sel.length; i++) + { + sel[i] = selected[i].isSelected(); + } + return sel; + } + + + void initialize(Model seed, int operation) + { + // we need a model with NO callbacks + stack.clear(); + this.operation = operation; + Random random = synth.random; + String[] keys = synth.getMutationKeys(); + + switch(operation) + { + case OPERATION_SEED_FROM_PATCH: + { + Model newSeed = seed.copy(); + double weight = blank.getModel().get("mutationrate", 0) / 100.0; + weight = weight * weight * weight; // make more sensitive at low end + int numMutations = 1; + + for(int i = 0; i < NUM_CANDIDATES; i++) + { + currentModels[i] = newSeed.copy(); + for(int j = 0; j < numMutations; j++) + currentModels[i] = currentModels[i].mutate(random, keys, weight); + if (i % 4 == 3) numMutations++; + } + + for(int i = 0; i < selected.length; i++) + selected[i].setSelected(true); + } + break; + case OPERATION_SEED_FROM_NUDGE: + { + double weight = blank.getModel().get("recombinationrate", 0) / 100.0; + for(int i = 0; i < 4; i++) + currentModels[i] = (Model)(synth.nudge[i].clone()); + int m = 4; + for(int i = 0; i < 4; i++) + for(int j = 0; j < 4; j++) + { + if (j == i) continue; + currentModels[m++] = currentModels[i].copy().crossover(random, currentModels[j], keys, weight); + } + // fill the next 16 + for(int i = 16; i < 32; i++) + { + // pick two parents, try to make them different from one another + int p1 = random.nextInt(16); + int p2 = 0; + for(int j = 0; j < 100; j++) + { + p2 = random.nextInt(16); + if (p2 != p1) break; + } + currentModels[i] = currentModels[p1].copy().crossover(random, currentModels[p1], keys, weight); + } + } + break; + case OPERATION_SEED_FROM_FOUR: + { + double weight = blank.getModel().get("recombinationrate", 0) / 100.0; + int m = 4; + for(int i = 0; i < 4; i++) + for(int j = 0; j < 4; j++) + { + if (j == i) continue; + currentModels[m++] = currentModels[i].copy().crossover(random, currentModels[j], keys, weight); + } + // fill the next 16 + for(int i = 16; i < 32; i++) + { + // pick two parents, try to make them different from one another + int p1 = random.nextInt(16); + int p2 = 0; + for(int j = 0; j < 100; j++) + { + p2 = random.nextInt(16); + if (p2 != p1) break; + } + currentModels[i] = currentModels[p1].copy().crossover(random, currentModels[p1], keys, weight); + } + } + break; + case OPERATION_SEED_FROM_SIX: + { + double weight = blank.getModel().get("recombinationrate", 0) / 100.0; + int m = 6; + for(int i = 0; i < 6; i++) + for(int j = 0; j < 6; j++) + { + if (j == i) continue; + if (m >= 32) break; + currentModels[m++] = currentModels[i].copy().crossover(random, currentModels[j], keys, weight); + } + } + break; + } + + pushStack(new int[] {-1, -1, -1}, new Model[] { seed, null, null }, getSelectedResults(), currentModels); + iterations.setName("Iteration " + stack.size()); + ratings[NUM_MODELS][0].setSelected(true); + ratings[NUM_MODELS][1].setSelected(true); + ratings[NUM_MODELS][2].setSelected(true); + repaint(); + } + + void shuffle(Random random, Model[] array, int start, int len) + { + for (int i = len - 1; i > 0; i--) + { + int index = random.nextInt(i + 1); + Model temp = array[start + index]; + array[start + index] = array[start + i]; + array[start + i] = temp; + } + } + + public static final double MUTATION_WEIGHT = 1.0; + + void produce(Random random, String[] keys, double recombination, double weight, Model a, Model b, Model c, Model oldA) + { + int numStages = NUM_CANDIDATES / STAGE_SIZE; + + for(int i = 0; i < numStages; i++) + { + produce(random, keys, recombination, weight, a, b, c, oldA, i * STAGE_SIZE); + } + + shuffle(random, currentModels, 0, STAGE_SIZE); + shuffle(random, currentModels, STAGE_SIZE, STAGE_SIZE); + } + + void produce(Random random, String[] keys, double recombination, double weight, Model a, Model b, Model c, Model oldA, int stage) + { + double mutationWeight = (stage/STAGE_SIZE + 1) * MUTATION_WEIGHT * weight; + + // A + B + currentModels[stage + 0] = a.copy().recombine(random, b, keys, recombination).mutate(random, keys, mutationWeight); + // A + C + currentModels[stage + 1] = a.copy().recombine(random, c, keys, recombination).mutate(random, keys, mutationWeight); + // A + (B + C) + currentModels[stage + 2] = a.copy().recombine(random, b.copy().recombine(random, c, keys, recombination), keys, recombination).mutate(random, keys, mutationWeight); + // A - B + currentModels[stage + 3] = a.copy().opposite(random, b, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + // A - C + currentModels[stage + 4] = a.copy().opposite(random, c, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + // A + currentModels[stage + 5] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + // B + currentModels[stage + 6] = b.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + // C + currentModels[stage + 7] = c.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + if ((stage + 8) < currentModels.length) + { + // A - Z + currentModels[stage + 8] = a.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight); + // B - A + currentModels[stage + 9] = b.copy().opposite(random, a, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + // C - A + currentModels[stage + 10] = c.copy().opposite(random, a, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + // B - C + currentModels[stage + 11] = b.copy().opposite(random, c, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + // C - B + currentModels[stage + 12] = c.copy().opposite(random, b, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + // B - Z + currentModels[stage + 13] = b.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight); + // C - Z + currentModels[stage + 14] = c.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight); + // B + C + currentModels[stage + 15] = b.copy().recombine(random, c, keys, recombination).mutate(random, keys, mutationWeight); + } + } + + void produce(Random random, String[] keys, double recombination, double weight, Model a, Model b, Model oldA) + { + int numStages = NUM_CANDIDATES / STAGE_SIZE; + + for(int i = 0; i < numStages; i++) + { + produce(random, keys, recombination, weight, a, b, oldA, i * STAGE_SIZE); + } + + shuffle(random, currentModels, 0, STAGE_SIZE); + shuffle(random, currentModels, STAGE_SIZE, STAGE_SIZE); + } + + void produce(Random random, String[] keys, double recombination, double weight, Model a, Model b, Model oldA, int stage) + { + double mutationWeight = (stage/STAGE_SIZE + 1) * MUTATION_WEIGHT * weight; + + // A + B + currentModels[stage + 0] = a.copy().recombine(random, b, keys, recombination).mutate(random, keys, mutationWeight); + currentModels[stage + 1] = a.copy().recombine(random, b, keys, recombination).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 2] = a.copy().recombine(random, b, keys, recombination).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + // A - B + currentModels[stage + 3] = a.copy().opposite(random, b, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 4] = a.copy().opposite(random, b, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + // B - A + currentModels[stage + 5] = b.copy().opposite(random, a, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 6] = b.copy().opposite(random, a, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + // A - Z + currentModels[stage + 7] = a.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight); + + if ((stage + 8) < currentModels.length) + { + currentModels[stage + 8] = a.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + // B - Z + currentModels[stage + 9] = b.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight); + currentModels[stage + 10] = b.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + // (A - Z) + (B - Z) + currentModels[stage + 11] = a.copy().opposite(random, oldA, keys, recombination, false).recombine(random, + b.copy().opposite(random, oldA, keys, recombination, false), keys, recombination).mutate(random, keys, mutationWeight); + + // A + currentModels[stage + 12] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 13] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + // B + currentModels[stage + 14] = b.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 15] = b.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + } + } + + void produce(Random random, String[] keys, double recombination, double weight, Model a, Model oldA) + { + int numStages = NUM_CANDIDATES / STAGE_SIZE; + + for(int i = 0; i < numStages; i++) + { + produce(random, keys, recombination, weight, a, oldA, i * STAGE_SIZE); + } + + shuffle(random, currentModels, 0, STAGE_SIZE); + shuffle(random, currentModels, STAGE_SIZE, STAGE_SIZE); + } + + void produce(Random random, String[] keys, double recombination, double weight, Model a, Model oldA, int stage) + { + double mutationWeight = (stage/STAGE_SIZE + 1) * MUTATION_WEIGHT * weight; + + // A + currentModels[stage + 0] = a.copy().mutate(random, keys, mutationWeight); + currentModels[stage + 1] = a.copy().mutate(random, keys, mutationWeight); + currentModels[stage + 2] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 3] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 4] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 5] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 6] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 7] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + if ((stage + 8) < currentModels.length) + { + currentModels[stage + 8] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 9] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 10] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 11] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 12] = a.copy().mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + + // A - Z + currentModels[stage + 13] = a.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight); + currentModels[stage + 14] = a.copy().opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + currentModels[stage + 15] = a.copy().opposite(random, oldA, keys, recombination, false).opposite(random, oldA, keys, recombination, false).mutate(random, keys, mutationWeight).mutate(random, keys, mutationWeight); + } + } + + + void constrict() + { + int poolSize= (bigger.isSelected() ? NUM_CANDIDATES : STAGE_SIZE); // that is, 32 vs 16 + Random random = synth.random; + String[] keys = synth.getMutationKeys(); + double weight = blank.getModel().get("recombinationrate", 0) / 100.0; + + + // Identify the individuals to replace and the ones to keep + int numToReplace = 0; + for(int i = 0; i < selected.length; i++) + { + if (!selected[i].isSelected()) numToReplace++; + } + int[] replace = new int[numToReplace]; + int[] keep = new int[poolSize - numToReplace]; + + if (replace.length == 0 || keep.length == 0) return; + + int k = 0; + int r = 0; + for(int i = 0; i < poolSize; i++) + { + if (selected[i].isSelected()) + keep[k++] = i; + else + replace[r++] = i; + } + + pushStack(new int[] { -1, -1, -1 }, new Model[] { currentModels[NUM_CANDIDATES - 1], null, null }, getSelectedResults(), currentModels); + operation = OPERATION_CONSTRICT; + + // Now replace the individuals + for(int i = 0; i < replace.length; i++) + { + // pick two parents, try to make them different from one another + int p1 = random.nextInt(keep.length); + int p2 = 0; + for(int j = 0; j < 100; j++) + { + p2 = random.nextInt(keep.length); + if (p2 != p1) break; + } + + if (method.getSelectedIndex() == 2) + { + // recombine + currentModels[replace[i]] = currentModels[keep[p1]].copy().recombine(random, currentModels[keep[p2]], keys, weight); + } + else + { + // cross over + currentModels[replace[i]] = currentModels[keep[p1]].copy().crossover(random, currentModels[keep[p2]], keys, weight); + } + } + + // Move the new ones to the beginning + Model[] old = (Model[])(currentModels.clone()); + int count = 0; + for(int i = 0; i < replace.length; i++) + { + currentModels[count++] = old[replace[i]]; + } + for(int i = 0; i < keep.length; i++) + { + currentModels[count++] = old[keep[i]]; + } + + iterations.setName("Iteration " + stack.size()); + repaint(); + + ratings[NUM_MODELS][0].setSelected(true); + ratings[NUM_MODELS][1].setSelected(true); + ratings[NUM_MODELS][2].setSelected(true); + for(int i = 0; i < NUM_CANDIDATES; i++) + selected[i].setSelected(true); + } + + + void climb(boolean determineBest) + { + Random random = synth.random; + String[] keys = synth.getMutationKeys(); + double weight = blank.getModel().get("mutationrate", 0) / 100.0; + + weight = weight * weight * weight; // make more sensitive at low end + + int[] bestModels = new int[3]; + + currentModels[NUM_MODELS - 1] = synth.getModel(); + + if (determineBest) + { + for(int j = 0; j < 3; j++) + bestModels[j] = -1; + + // load the best models + for(int i = 0; i < NUM_MODELS; i++) + { + for(int j = 0; j < 3; j++) + { + if (ratings[i][j].isSelected()) + bestModels[j] = i; + } + } + } + + if (bestModels[0] == -1) + { + bestModels[0] = bestModels[1]; + bestModels[1] = bestModels[2]; + bestModels[2] = -1; + } + if (bestModels[0] == -1) + { + bestModels[0] = bestModels[1]; + bestModels[1] = bestModels[2]; + bestModels[2] = -1; + } + if (bestModels[1] == -1) + { + bestModels[1] = bestModels[2]; + bestModels[2] = -1; + } + + boolean zeroModels = false; + Model oldA = topStack().parents[0]; + + if (bestModels[0] == -1) + { + again(); + zeroModels = true; + } + else if (bestModels[1] == -1) + { + pushStack(bestModels, new Model[] { currentModels[bestModels[0]], null, null }, getSelectedResults(), currentModels); + produce(random, keys, STANDARD_RECOMBINATION_RATE / 100.0, weight, currentModels[bestModels[0]], oldA); + operation = OPERATION_CLIMB; + } + else if (bestModels[2] == -1) + { + pushStack(bestModels, new Model[] { currentModels[bestModels[0]], currentModels[bestModels[1]], null }, getSelectedResults(), currentModels); + produce(random, keys, STANDARD_RECOMBINATION_RATE / 100.0, weight, currentModels[bestModels[0]], currentModels[bestModels[1]], oldA); + operation = OPERATION_CLIMB; + } + else + { + pushStack(bestModels, new Model[] { currentModels[bestModels[0]], currentModels[bestModels[1]], currentModels[bestModels[2]] }, getSelectedResults(), currentModels); + produce(random, keys, STANDARD_RECOMBINATION_RATE / 100.0, weight, currentModels[bestModels[0]], currentModels[bestModels[1]], currentModels[bestModels[2]], oldA); + operation = OPERATION_CLIMB; + } + + if (!zeroModels) + { + iterations.setName("Iteration " + stack.size()); + repaint(); + + ratings[NUM_MODELS][0].setSelected(true); + ratings[NUM_MODELS][1].setSelected(true); + ratings[NUM_MODELS][2].setSelected(true); + } + } + } + + diff --git a/edisyn/Mac.java b/edisyn/Mac.java new file mode 100644 index 00000000..d13abd7a --- /dev/null +++ b/edisyn/Mac.java @@ -0,0 +1,76 @@ +/*** + Copyright 2017 by Sean Luke + Licensed under the Apache License version 2.0 +*/ + +package edisyn; + +import java.lang.reflect.*; + +// Code for handling MacOS X specific About Menus. Maybe also we'll handle Preferences later. +// +// Largely cribbed from https://stackoverflow.com/questions/7256230/in-order-to-macify-a-java-app-to-catch-the-about-event-do-i-have-to-implement +// +// I used the reflection version so it compiles cleanly on linux and windows as well. + +public class Mac + { + public static void setup(Synth synth) + { + if (System.getProperty("os.name").contains("Mac")) + { + try + { + Object app = Class.forName("com.apple.eawt.Application").getMethod("getApplication").invoke(null); + + Object al = Proxy.newProxyInstance( + Class.forName("com.apple.eawt.AboutHandler").getClassLoader(), + new Class[]{Class.forName("com.apple.eawt.AboutHandler")}, + new AboutListener(synth)); + + app.getClass().getMethod("setAboutHandler", Class.forName("com.apple.eawt.AboutHandler")).invoke(app, al); + + al = Proxy.newProxyInstance( + Class.forName("com.apple.eawt.QuitHandler").getClassLoader(), + new Class[]{Class.forName("com.apple.eawt.QuitHandler")}, + new QuitListener(synth)); + + app.getClass().getMethod("setQuitHandler", Class.forName("com.apple.eawt.QuitHandler")).invoke(app, al); + } + catch (Exception e) + { + //fail quietly + } + } + } + } + +class AboutListener implements InvocationHandler + { + Synth synth; + public AboutListener(Synth synth) + { + this.synth = synth; + } + + public Object invoke(Object proxy, Method method, Object[] args) + { + synth.doAbout(); + return null; + } + } + +class QuitListener implements InvocationHandler + { + Synth synth; + public QuitListener(Synth synth) + { + this.synth = synth; + } + + public Object invoke(Object proxy, Method method, Object[] args) + { + synth.doQuit(); + return null; + } + } diff --git a/edisyn/Manufacturers.txt b/edisyn/Manufacturers.txt new file mode 100644 index 00000000..d67e81f1 --- /dev/null +++ b/edisyn/Manufacturers.txt @@ -0,0 +1,1175 @@ +### This data is thanks to help from the MIDI Association. +### -- Sean + +00 00 01 +Time/Warner Interactive +00 00 02 +Advanced Gravis Comp. Tech Ltd. +00 00 03 +Media Vision +00 00 04 +Dornes Research Group +00 00 05 +K-Muse +00 00 06 +Stypher +00 00 07 +Digital Music Corp. +00 00 08 +IOTA Systems +00 00 09 +New England Digital +00 00 0A +Artisyn +00 00 0B +IVL Technologies Ltd. +00 00 0C +Southern Music Systems +00 00 0D +Lake Butler Sound Company +00 00 0E +Alesis Studio Electronics +00 00 0F +Sound Creation +00 00 01 +Time/Warner Interactive +00 00 10 +DOD Electronics Corp. +00 00 11 +Studer-Editech +00 00 12 +Sonus +00 00 13 +Temporal Acuity Products +00 00 14 +Perfect Fretworks +00 00 15 +KAT Inc. +00 00 16 +Opcode Systems +00 00 17 +Rane Corporation +00 00 18 +Anadi Electronique +00 00 19 +KMX +00 00 1A +Allen & Heath Brenell +00 00 1B +Peavey Electronics +00 00 1C +360 Systems +00 00 1D +Spectrum Design and Development +00 00 1E +Marquis Music +00 00 1F +Zeta Systems +00 00 20 +Axxes (Brian Parsonett) +00 00 21 +Orban +00 00 22 +Indian Valley Mfg. +00 00 23 +Triton +00 00 24 +KTI +00 00 25 +Breakaway Technologies +00 00 26 +Leprecon / CAE Inc. +00 00 27 +Harrison Systems Inc. +00 00 28 +Future Lab/Mark Kuo +00 00 29 +Rocktron Corporation +00 00 2A +PianoDisc +00 00 2B +Cannon Research Group +00 00 2C +Reserved +00 00 2D +Rodgers Instrument LLC +00 00 2E +Blue Sky Logic +00 00 2F +Encore Electronics +00 00 30 +Uptown +00 00 31 +Voce +00 00 32 +CTI Audio, Inc. (Musically Intel. Devs.) +00 00 33 +S3 Incorporated +00 00 34 +Broderbund / Red Orb +00 00 35 +Allen Organ Co. +00 00 36 +Reserved +00 00 37 +Music Quest +00 00 38 +Aphex +00 00 39 +Gallien Krueger +00 00 3A +IBM +00 00 3B +Mark Of The Unicorn +00 00 3C +Hotz Corporation +00 00 3D +ETA Lighting +00 00 3E +NSI Corporation +00 00 3F +Ad Lib, Inc. +00 00 40 +Richmond Sound Design +00 00 41 +Microsoft +00 00 42 +Mindscape (Software Toolworks) +00 00 43 +Russ Jones Marketing / Niche +00 00 44 +Intone +00 00 45 +Advanced Remote Technologies +00 00 46 +White Instruments +00 00 47 +GT Electronics/Groove Tubes +00 00 48 +Pacific Research & Engineering +00 00 49 +Timeline Vista, Inc. +00 00 4A +Mesa Boogie Ltd. +00 00 4B +FSLI +00 00 4C +Sequoia Development Group +00 00 4D +Studio Electronics +00 00 4E +Euphonix, Inc +00 00 4F +InterMIDI, Inc. +00 00 50 +MIDI Solutions Inc. +00 00 51 +3DO Company +00 00 52 +Lightwave Research / High End Systems +00 00 53 +Micro-W Corporation +00 00 54 +Spectral Synthesis, Inc. +00 00 55 +Lone Wolf +00 00 56 +Studio Technologies Inc. +00 00 57 +Peterson Electro-Musical Product, Inc. +00 00 58 +Atari Corporation +00 00 59 +Marion Systems Corporation +00 00 5A +Design Event +00 00 5B +Winjammer Software Ltd. +00 00 5C +AT&T Bell Laboratories +00 00 5D +Reserved +00 00 5E +Symetrix +00 00 5F +MIDI the World +00 00 60 +Spatializer +00 00 61 +Micros 'N MIDI +00 00 62 +Accordians International +00 00 63 +EuPhonics (now 3Com) +00 00 64 +Musonix +00 00 65 +Turtle Beach Systems (Voyetra) +00 00 66 +Loud Technologies / Mackie +00 00 67 +Compuserve +00 00 68 +BEC Technologies +00 00 69 +QRS Music Inc +00 00 6A +P.G. Music +00 00 6B +Sierra Semiconductor +00 00 6C +EpiGraf +00 00 6D +Electronics Diversified Inc +00 00 6E +Tune 1000 +00 00 6F +Advanced Micro Devices +00 00 70 +Mediamation +00 00 71 +Sabine Musical Mfg. Co. Inc. +00 00 72 +Woog Labs +00 00 73 +Micropolis Corp +00 00 74 +Ta Horng Musical Instrument +00 00 75 +e-Tek Labs (Forte Tech) +00 00 76 +Electro-Voice +00 00 77 +Midisoft Corporation +00 00 78 +QSound Labs +00 00 79 +Westrex +00 00 7A +Nvidia +00 00 7B +ESS Technology +00 00 7C +Media Trix Peripherals +00 00 7D +Brooktree Corp +00 00 7E +Otari Corp +00 00 7F +Key Electronics, Inc. +00 01 00 +Shure Incorporated +00 01 01 +AuraSound +00 01 02 +Crystal Semiconductor +00 01 03 +Conexant (Rockwell) +00 01 04 +Silicon Graphics +00 01 05 +M-Audio (Midiman) +00 01 06 +PreSonus +00 01 08 +Topaz Enterprises +00 01 09 +Cast Lighting +00 01 0A +Microsoft +00 01 0B +Sonic Foundry +00 01 0C +Line 6 (Fast Forward) (Yamaha) +00 01 0D +Beatnik Inc +00 01 0E +Van Koevering Company +00 01 0F +Altech Systems +00 01 10 +S & S Research +00 01 11 +VLSI Technology +00 01 12 +Chromatic Research +00 01 13 +Sapphire +00 01 14 +IDRC +00 01 15 +Justonic Tuning +00 01 16 +TorComp Research Inc. +00 01 17 +Newtek Inc. +00 01 18 +Sound Sculpture +00 01 19 +Walker Technical +00 01 1A +Digital Harmony (PAVO) +00 01 1B +InVision Interactive +00 01 1C +T-Square Design +00 01 1D +Nemesys Music Technology +00 01 1E +DBX Professional (Harman Intl) +00 01 1F +Syndyne Corporation +00 01 20 +Bitheadz +00 01 21 +Cakewalk Music Software (Gibson) +00 01 22 +Analog Devices +00 01 23 +National Semiconductor +00 01 24 +Boom Theory / Adinolfi Alternative Percussion +00 01 25 +Virtual DSP Corporation +00 01 26 +Antares Systems +00 01 27 +Angel Software +00 01 28 +St Louis Music +00 01 29 +Passport Music Software LLC (Gvox) +00 01 2A +Ashley Audio Inc. +00 01 2B +Vari-Lite Inc. +00 01 2C +Summit Audio Inc. +00 01 2D +Aureal Semiconductor Inc. +00 01 2E +SeaSound LLC +00 01 2F +U.S. Robotics +00 01 30 +Aurisis Research +00 01 31 +Nearfield Research +00 01 32 +FM7 Inc +00 01 33 +Swivel Systems +00 01 34 +Hyperactive Audio Systems +00 01 35 +MidiLite (Castle Studios Productions) +00 01 36 +Radikal Technologies +00 01 37 +Roger Linn Design +00 01 38 +TC-Helicon Vocal Technologies +00 01 39 +Event Electronics +00 01 3A +Sonic Network Inc +00 01 3B +Realtime Music Solutions +00 01 3C +Apogee Digital +00 01 3D +Classical Organs, Inc. +00 01 3E +Microtools Inc. +00 01 3F +Numark Industries +00 01 40 +Frontier Design Group, LLC +00 01 41 +Recordare LLC +00 01 42 +Starr Labs +00 01 43 +Voyager Sound Inc. +00 01 44 +Manifold Labs +00 01 45 +Aviom Inc. +00 01 46 +Mixmeister Technology +00 01 47 +Notation Software +00 01 48 +Mercurial Communications +00 01 49 +Wave Arts +00 01 4A +Logic Sequencing Devices +00 01 4B +Axess Electronics +00 01 4C +Muse Research +00 01 4D +Open Labs +00 01 4E +Guillemot Corp +00 01 4F +Samson Technologies +00 01 50 +Electronic Theatre Controls +00 01 51 +Blackberry (RIM) +00 01 52 +Mobileer +00 01 53 +Synthogy +00 01 54 +Lynx Studio Technology Inc. +00 01 55 +Damage Control Engineering LLC +00 01 56 +Yost Engineering, Inc. +00 01 57 +Brooks & Forsman Designs LLC / DrumLite +00 01 58 +Infinite Response +00 01 59 +Garritan Corp +00 01 5A +Plogue Art et Technologie, Inc +00 01 5B +RJM Music Technology +00 01 5C +Custom Solutions Software +00 01 5D +Sonarcana LLC / Highly Liquid +00 01 5E +Centrance +00 01 5F +Kesumo LLC +00 01 60 +Stanton (Gibson Brands) +00 01 61 +Livid Instruments +00 01 62 +First Act / 745 Media +00 01 63 +Pygraphics, Inc. +00 01 64 +Panadigm Innovations Ltd +00 01 65 +Avedis Zildjian Co +00 01 66 +Auvital Music Corp +00 01 67 +You Rock Guitar (was: Inspired Instruments) +00 01 68 +Chris Grigg Designs +00 01 69 +Slate Digital LLC +00 01 6A +Mixware +00 01 6B +Social Entropy +00 01 6C +Source Audio LLC +00 01 6D +Ernie Ball / Music Man +00 01 6E +Fishman +00 01 6F +Custom Audio Electronics +00 01 70 +American Audio/DJ +00 01 71 +Mega Control Systems +00 01 72 +Kilpatrick Audio +00 01 73 +iConnectivity +00 01 74 +Fractal Audio +00 01 75 +NetLogic Microsystems +00 01 76 +Music Computing +00 01 77 +Nektar Technology Inc +00 01 78 +Zenph Sound Innovations +00 01 79 +DJTechTools.com +00 01 7A +Rezonance Labs +00 01 7B +Decibel Eleven +00 01 7C +CNMAT +00 01 7D +Media Overkill +00 01 7E +Confusion Studios +00 01 7F +moForte Inc +00 02 00 +Miselu Inc +00 02 01 +Amelia's Compass LLC +00 02 02 +Zivix LLC +00 02 03 +Artiphon +00 02 04 +Synclavier Digital +00 02 05 +Light & Sound Control Devices LLC +00 02 06 +Retronyms Inc +00 02 07 +JS Technologies +00 02 08 +Quicco Sound +00 02 09 +A-Designs Audio +00 02 0A +McCarthy Music Corp +00 02 0B +Denon DJ +00 02 0C +Keith Robert Murray +00 02 0D +Google +00 02 0E +ISP Technologies +00 02 0F +Abstrakt Instruments LLC +00 02 10 +Meris LLC +00 02 11 +Sensorpoint LLC +00 02 12 +Hi-Z Labs +00 02 13 +Imitone +00 02 14 +Intellijel Designs Inc. +00 02 15 +Dasz Instruments Inc. +00 02 16 +Remidi +00 02 17 +Disaster Area Designs LLC +00 02 18 +Universal Audio +00 02 19 +Carter Duncan Corp +00 02 1A +Essential Technology +00 02 1B +Cantux Research LLC +00 02 1C +Hummel Technologies +00 02 1D +Sensel Inc +00 02 1E +DBML Group +00 02 1F +Madrona Labs +00 02 20 +Mesa Boogie +00 02 21 +Effigy Labs +00 02 22 +MK2 Image Ltd +00 20 00 +Dream SAS +00 20 01 +Strand Lighting +00 20 02 +Amek Div of Harman Industries +00 20 03 +Casa Di Risparmio Di Loreto +00 20 04 +Böhm electronic GmbH +00 20 05 +Syntec Digital Audio +00 20 06 +Trident Audio Developments +00 20 07 +Real World Studio +00 20 08 +Evolution Synthesis, Ltd +00 20 09 +Yes Technology +00 20 0A +Audiomatica +00 20 0B +Bontempi SpA (Sigma) +00 20 0C +F.B.T. Elettronica SpA +00 20 0D +MidiTemp GmbH +00 20 0E +LA Audio (Larking Audio) +00 20 0F +Zero 88 Lighting Limited +00 20 10 +Micon Audio Electronics GmbH +00 20 11 +Forefront Technology +00 20 12 +Studio Audio and Video Ltd. +00 20 13 +Kenton Electronics +00 20 14 +Celco/ Electrosonic +00 20 15 +ADB +00 20 16 +Marshall Products Limited +00 20 17 +DDA +00 20 18 +BSS Audio Ltd. +00 20 19 +MA Lighting Technology +00 20 1A +Fatar SRL c/o Music Industries +00 20 1B +QSC Audio Products Inc. +00 20 1C +Artisan Clasic Organ Inc. +00 20 1D +Orla Spa +00 20 1E +Pinnacle Audio (Klark Teknik PLC) +00 20 1F +TC Electronics +00 20 20 +Doepfer Musikelektronik GmbH +00 20 21 +Creative ATC / E-mu +00 20 22 +Seyddo/Minami +00 20 23 +LG Electronics (Goldstar) +00 20 24 +Midisoft sas di M.Cima & C +00 20 25 +Samick Musical Inst. Co. Ltd. +00 20 26 +Penny and Giles (Bowthorpe PLC) +00 20 27 +Acorn Computer +00 20 28 +LSC Electronics Pty. Ltd. +00 20 29 +Focusrite/Novation +00 20 2A +Samkyung Mechatronics +00 20 2B +Medeli Electronics Co. +00 20 2C +Charlie Lab SRL +00 20 2D +Blue Chip Music Technology +00 20 2E +BEE O Corp +00 20 2F +LG Semicon America +00 20 30 +TESI +00 20 31 +EMAGIC +00 20 32 +Behringer GmbH +00 20 33 +Access Music Electronics +00 20 34 +Synoptic +00 20 35 +Hanmesoft +00 20 36 +Terratec Electronic GmbH +00 20 37 +Proel SpA +00 20 38 +IBK MIDI +00 20 39 +IRCAM +00 20 3A +Propellerhead Software +00 20 3B +Red Sound Systems Ltd +00 20 3C +Elektron ESI AB +00 20 3D +Sintefex Audio +00 20 3E +MAM (Music and More) +00 20 3F +Amsaro GmbH +00 20 40 +CDS Advanced Technology BV (Lanbox) +00 20 41 +Mode Machines (Touched By Sound GmbH) +00 20 42 +DSP Arts +00 20 43 +Phil Rees Music Tech +00 20 44 +Stamer Musikanlagen GmbH +00 20 45 +Musical Muntaner S.A. dba Soundart +00 20 46 +C-Mexx Software +00 20 47 +Klavis Technologies +00 20 48 +Noteheads AB +00 20 49 +Algorithmix +00 20 4A +Skrydstrup R&D +00 20 4B +Professional Audio Company +00 20 4C +NewWave Labs (MadWaves) +00 20 4D +Vermona +00 20 4E +Nokia +00 20 4F +Wave Idea +00 20 50 +Hartmann GmbH +00 20 51 +Lion's Tracs +00 20 52 +Analogue Systems +00 20 53 +Focal-JMlab +00 20 54 +Ringway Electronics (Chang-Zhou) Co Ltd +00 20 55 +Faith Technologies (Digiplug) +00 20 56 +Showworks +00 20 57 +Manikin Electronic +00 20 58 +1 Come Tech +00 20 59 +Phonic Corp +00 20 5A +Dolby Australia (Lake) +00 20 5B +Silansys Technologies +00 20 5C +Winbond Electronics +00 20 5D +Cinetix Medien und Interface GmbH +00 20 5E +A&G Soluzioni Digitali +00 20 5F +Sequentix Music Systems +00 20 60 +Oram Pro Audio +00 20 61 +Be4 Ltd +00 20 62 +Infection Music +00 20 63 +Central Music Co. (CME) +00 20 64 +genoQs Machines GmbH +00 20 65 +Medialon +00 20 66 +Waves Audio Ltd +00 20 67 +Jerash Labs +00 20 68 +Da Fact +00 20 69 +Elby Designs +00 20 6A +Spectral Audio +00 20 6B +Arturia +00 20 6C +Vixid +00 20 6D +C-Thru Music +00 20 6E +Ya Horng Electronic Co LTD +00 20 6F +SM Pro Audio +00 20 70 +OTO Machines +00 20 71 +ELZAB S.A. (G LAB) +00 20 72 +Blackstar Amplification Ltd +00 20 73 +M3i Technologies GmbH +00 20 74 +Gemalto (from Xiring) +00 20 75 +Prostage SL +00 20 76 +Teenage Engineering +00 20 77 +Tobias Erichsen Consulting +00 20 78 +Nixer Ltd +00 20 79 +Hanpin Electron Co Ltd +00 20 7A +"MIDI-hardware" R.Sowa +00 20 7B +Beyond Music Industrial Ltd +00 20 7C +Kiss Box B.V. +00 20 7D +Misa Digital Technologies Ltd +00 20 7E +AI Musics Technology Inc +00 20 7F +Serato Inc LP +00 21 00 +Limex +00 21 01 +Kyodday (Tokai) +00 21 02 +Mutable Instruments +00 21 03 +PreSonus Software Ltd +00 21 04 +Ingenico (was Xiring) +00 21 05 +Fairlight Instruments Pty Ltd +00 21 06 +Musicom Lab +00 21 07 +Modal Electronics (Modulus/VacoLoco) +00 21 08 +RWA (Hong Kong) Limited +00 21 09 +Native Instruments +00 21 0A +Naonext +00 21 0B +MFB +00 21 0C +Teknel Research +00 21 0D +Ploytec GmbH +00 21 0E +Surfin Kangaroo Studio +00 21 0F +Philips Electronics HK Ltd +00 21 10 +ROLI Ltd +00 21 11 +Panda-Audio Ltd +00 21 12 +BauM Software +00 21 13 +Machinewerks Ltd. +00 21 14 +Xiamen Elane Electronics +00 21 15 +Marshall Amplification PLC +00 21 16 +Kiwitechnics Ltd +00 21 17 +Rob Papen +00 21 18 +Spicetone OU +00 21 19 +V3Sound +00 21 1A +IK Multimedia +00 21 1B +Novalia Ltd +00 21 1C +Modor Music +00 21 1D +Ableton +00 21 1E +Dtronics +00 21 1F +ZAQ Audio +00 21 20 +Muabaobao Education Technology Co Ltd +00 21 21 +Flux Effects +00 21 22 +Audiothingies (MCDA) +00 21 23 +Retrokits +00 21 24 +Morningstar FX Pte Ltd +00 21 25 +Hotone Audio +00 21 26 +Expressive +00 21 27 +Expert Sleepers Ltd +00 21 28 +Timecode-Vision Technology +00 21 29 +Hornberg Research GbR +00 21 2A +Sonic Potions +00 21 2B +Audiofront +00 21 2C +Fred's Lab +00 21 2D +Audio Modeling +00 21 2E +C. Bechstein Digital GmbH +00 21 2F +Motas Electronics Ltd +00 40 00 +Crimson Technology Inc. +00 40 01 +Softbank Mobile Corp +00 40 03 +D&M Holdings Inc. +01 +Sequential +02 +IDP +03 +Voyetra Turtle Beach, Inc. +04 +Moog Music +05 +Passport Designs +06 +Lexicon Inc. +07 +Kurzweil / Young Chang +08 +Fender +09 +MIDI9 +0A +AKG Acoustics +0B +Voyce Music +0C +WaveFrame (Timeline) +0D +ADA Signal Processors, Inc. +0E +Garfield Electronics +0F +Ensoniq +10 +Oberheim / Gibson Labs +11 +Apple, Inc. +12 +Grey Matter Response +13 +Digidesign Inc. +14 +Palmtree Instruments +15 +JLCooper Electronics +16 +Lowrey Organ Company +17 +Adams-Smith +18 +E-mu +19 +Harmony Systems +1A +ART +1B +Baldwin +1C +Eventide +1D +Inventronics +1E +Key Concepts +1F +Clarity +20 +Passac +21 +Proel Labs (SIEL) +22 +Synthaxe (UK) +23 +Stepp +24 +Hohner +25 +Twister +26 +Ketron s.r.l. +27 +Jellinghaus MS +28 +Southworth Music Systems +29 +PPG (Germany) +2A +JEN +2B +Solid State Logic Organ Systems +2C +Audio Veritrieb-P. Struven +2D +Neve +2E +Soundtracs Ltd. +2F +Elka +30 +Dynacord +31 +Viscount International Spa (Intercontinental Electronics) +32 +Drawmer +33 +Clavia Digital Instruments +34 +Audio Architecture +35 +Generalmusic Corp SpA +36 +Cheetah Marketing +37 +C.T.M. +38 +Simmons UK +39 +Soundcraft Electronics +3A +Steinberg Media Technologies AG +3B +Wersi GmbH +3C +AVAB Niethammer AB +3D +Digigram +3E +Waldorf Electronics GmbH +3F +Quasimidi +40 +Kawai Musical Instruments MFG. CO. Ltd +41 +Roland Corporation +42 +Korg Inc. +43 +Yamaha Corporation +44 +Casio Computer Co. Ltd +46 +Kamiya Studio Co. Ltd +47 +Akai Electric Co. Ltd. +48 +Victor Company of Japan, Ltd. +4B +Fujitsu Limited +4C +Sony Corporation +4E +Teac Corporation +50 +Matsushita Electric Industrial Co. , Ltd +51 +Fostex Corporation +52 +Zoom Corporation +54 +Matsushita Communication Industrial Co., Ltd. +55 +Suzuki Musical Instruments MFG. Co., Ltd. +56 +Fuji Sound Corporation Ltd. +57 +Acoustic Technical Laboratory, Inc. +59 +Faith, Inc. +5A +Internet Corporation +5C +Seekers Co. Ltd. +5F +SD Card Association +60 +Reserved +61 +Reserved +62 +Reserved +63 +Reserved +64 +Reserved +65 +Reserved +66 +Reserved +67 +Reserved +68 +Reserved +69 +Reserved +6A +Reserved +6B +Reserved +6C +Reserved +6D +Reserved +6E +Reserved +6F +Reserved +70 +Reserved +71 +Reserved +72 +Reserved +73 +Reserved +74 +Reserved +75 +Reserved +76 +Reserved +77 +Reserved +78 +Reserved +79 +Reserved +7A +Reserved +7B +Reserved +7C +Reserved +7D +Reserved for Private, Test, and Educational Use +7E +Reserved for Sample Dumps, Tuning Tables, etc. +7F +Reserved for Midi Time Code, Midi Machine Control, etc. diff --git a/edisyn/Midi.java b/edisyn/Midi.java index 17f18131..648dd22a 100644 --- a/edisyn/Midi.java +++ b/edisyn/Midi.java @@ -87,14 +87,22 @@ public MidiDeviceWrapper(MidiDevice device) public String toString() { String desc = device.getDeviceInfo().getDescription().trim(); - + String name = device.getDeviceInfo().getName(); + if (name == null) + name = ""; + if (desc == null || desc.equals("")) + desc = "MIDI Device"; + // All CoreMIDI4J names begin with "CoreMIDI4J - " - String name = device.getDeviceInfo().getName().substring(13).trim(); + if (name.startsWith("CoreMIDI4J - ")) + name = name.substring(13).trim(); + else + name = name.trim(); if (name.equals("")) - return desc; + return desc.trim(); else - return desc + ": " + name; + return name; } Transmitter transmitter; @@ -147,16 +155,41 @@ public Receiver getReceiver() } - static ArrayList allDevices; - static ArrayList inDevices; - static ArrayList outDevices; - static ArrayList keyDevices; - - static + static Object findDevice(String name, ArrayList devices) { - MidiDevice.Info[] midiDevices = uk.co.xfactorylibrarians.coremidi4j.CoreMidiDeviceProvider.getMidiDeviceInfo(); + if (name == null) return null; + for(int i = 0; i < devices.size(); i++) + { + if (devices.get(i) instanceof String) + { + if (((String)devices.get(i)).equals(name)) + return devices.get(i); + } + else + { + MidiDeviceWrapper mdn = (MidiDeviceWrapper)(devices.get(i)); + if (mdn.toString().equals(name)) + return mdn; + } + } + return null; + } - allDevices = new ArrayList(); + + static void updateDevices() + { + MidiDevice.Info[] midiDevices; + + try + { + midiDevices = uk.co.xfactorylibrarians.coremidi4j.CoreMidiDeviceProvider.getMidiDeviceInfo(); + } + catch (Exception ex) + { + midiDevices = MidiSystem.getMidiDeviceInfo(); + } + + ArrayList allDevices = new ArrayList(); for(int i = 0; i < midiDevices.length; i++) { try @@ -173,6 +206,35 @@ public Receiver getReceiver() } catch(Exception e) { } } + + // Do they hold the same exact devices? + if (Midi.allDevices != null && Midi.allDevices.size() == allDevices.size()) + { + Set set = new HashSet(); + for(int i = 0; i < Midi.allDevices.size(); i++) + { + set.add(((MidiDeviceWrapper)(Midi.allDevices.get(i))).device); + } + + boolean same = true; + for(int i = 0; i < allDevices.size(); i++) + { + if (!set.contains(((MidiDeviceWrapper)(allDevices.get(i))).device)) + { + same = false; // something's different + break; + } + } + + if (same) + { + return; // they're identical + } + } + + // at this point allDevices isn't the same as Midi.allDevices, so set it and update + Midi.allDevices = allDevices; + inDevices = new ArrayList(); keyDevices = new ArrayList(); @@ -206,7 +268,15 @@ public Receiver getReceiver() } } - + static ArrayList allDevices; + static ArrayList inDevices; + static ArrayList outDevices; + static ArrayList keyDevices; + + static + { + updateDevices(); + } public static class Tuple { @@ -231,48 +301,96 @@ public static class Tuple /** The current keyboard/controller input */ public Thru key; /** The current keyboard/controller input device's wrapper */ - public MidiDeviceWrapper keyName; + public MidiDeviceWrapper keyWrap; /** The current receiver which is attached to the keyboard/controller input to perform its commands. Typically generated with Synth.buildKeyReceiver() */ public Receiver keyReceiver; /** The channel to receive voiced messages from on the keyboard/controller input. */ public int keyChannel = KEYCHANNEL_OMNI; + + public String id = "0"; int refcount = 1; - - public Tuple copy(Receiver inReceiver, Receiver keyReceiver) - { - refcount++; - + + public Tuple copy(Receiver inReceiver, Receiver keyReceiver) + { + if (refcount < 1) + throw new RuntimeException("Cannot copy a fully disposed Midi tuple"); + + refcount++; + if (in != null) - in.addReceiver(inReceiver); - + in.addReceiver(inReceiver); + if (key != null) - key.addReceiver(keyReceiver); - - return this; - } - + key.addReceiver(keyReceiver); + + return this; + } + public void dispose() { refcount--; + if (refcount == 0) - { - if (key != null && keyReceiver != null) - key.removeReceiver(keyReceiver); - if (in != null && inReceiver!= null) - in.removeReceiver(inReceiver); - } - if (refcount <= 0) - { - key = null; - keyReceiver = null; - in = null; - inReceiver = null; - } + { + if (key != null && keyReceiver != null) + key.removeReceiver(keyReceiver); + if (in != null && inReceiver!= null) + in.removeReceiver(inReceiver); + + // We don't close() stuff because of prior MIDI bugs in coremidi4j which are getting fixed (I believe). + // At any rate, the only time we will see a closed Receiver or a Thru is if it closes itself, because we + // share them. And when we quit, we just leak (probably can't help that anyway on a hard-quit). + // Hope that's okay. + + key = null; + keyReceiver = null; + in = null; + inReceiver = null; + } } } + static void setLastTupleIn(String path, Synth synth) { Synth.setLastX(path, "LastTupleIn", synth.getSynthNameLocal(), false); } + static String getLastTupleIn(Synth synth) { return Synth.getLastX("LastTupleIn", synth.getSynthNameLocal(), false); } + + static void setLastTupleOut(String path, Synth synth) { Synth.setLastX(path, "LastTupleOut", synth.getSynthNameLocal(), false); } + static String getLastTupleOut(Synth synth) { return Synth.getLastX("LastTupleOut", synth.getSynthNameLocal(), false); } + + static void setLastTupleKey(String path, Synth synth) { Synth.setLastX(path, "LastTupleKey", synth.getSynthNameLocal(), false); } + static String getLastTupleKey(Synth synth) { return Synth.getLastX("LastTupleKey", synth.getSynthNameLocal(), false); } + + static void setLastTupleOutChannel(int channel, Synth synth) { Synth.setLastX("" + channel, "LastTupleOutChannel", synth.getSynthNameLocal(), false); } + static int getLastTupleOutChannel(Synth synth) + { + String val = Synth.getLastX("LastTupleOutChannel", synth.getSynthNameLocal(), false); + if (val == null) return -1; + else + { + try + { return Integer.parseInt(val); } + catch (Exception e) + { e.printStackTrace(); return -1; } + } + } + + static void setLastTupleKeyChannel(int channel, Synth synth) { Synth.setLastX("" + channel, "LastTupleKeyChannel", synth.getSynthNameLocal(), false); } + static int getLastTupleKeyChannel(Synth synth) + { + String val = Synth.getLastX("LastTupleKeyChannel", synth.getSynthNameLocal(), false); + if (val == null) return -1; + else + { + try + { return Integer.parseInt(val); } + catch (Exception e) + { e.printStackTrace(); return -1; } + } + } + + + public static final Tuple CANCELLED = new Tuple(); public static final Tuple FAILED = new Tuple(); @@ -280,22 +398,28 @@ public void dispose() You may provide the old tuple for defaults or pass in null. You also provide the inReceiver and keyReceiver to be attached to the input and keyboard/controller input. You get these with Synth.buildKeyReceiver() and Synth.buildInReceiver() */ - public static Tuple getNewTuple(Tuple old, JComponent root, String message, Receiver inReceiver, Receiver keyReceiver) + public static Tuple getNewTuple(Tuple old, Synth synth, String message, Receiver inReceiver, Receiver keyReceiver) { + updateDevices(); + if (inDevices.size() == 0) { - JOptionPane.showOptionDialog(root, "There are no MIDI devices available to receive from.", + synth.disableMenuBar(); + JOptionPane.showOptionDialog(synth, "There are no MIDI devices available to receive from.", "Cannot Connect", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[] { "Run Disconnected" }, "Run Disconnected"); + synth.enableMenuBar(); return CANCELLED; } else if (outDevices.size() == 0) { - JOptionPane.showOptionDialog(root, "There are no MIDI devices available to send to.", + synth.disableMenuBar(); + JOptionPane.showOptionDialog(synth, "There are no MIDI devices available to send to.", "Cannot Connect", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[] { "Run Disconnected" }, "Run Disconnected"); + synth.enableMenuBar(); return CANCELLED; } else @@ -304,28 +428,55 @@ else if (outDevices.size() == 0) String[] rc = new String[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16" }; JComboBox inCombo = new JComboBox(inDevices.toArray()); + inCombo.setMaximumRowCount(32); if (old != null && old.inWrap != null && inDevices.indexOf(old.inWrap) != -1) inCombo.setSelectedIndex(inDevices.indexOf(old.inWrap)); + else if (findDevice(getLastTupleIn(synth), inDevices) != null) + inCombo.setSelectedItem(findDevice(getLastTupleIn(synth), inDevices)); JComboBox outCombo = new JComboBox(outDevices.toArray()); + outCombo.setMaximumRowCount(32); if (old != null && old.outWrap != null && outDevices.indexOf(old.outWrap) != -1) outCombo.setSelectedIndex(outDevices.indexOf(old.outWrap)); + else if (findDevice(getLastTupleOut(synth), outDevices) != null) + outCombo.setSelectedItem(findDevice(getLastTupleOut(synth), outDevices)); JComboBox keyCombo = new JComboBox(keyDevices.toArray()); + keyCombo.setMaximumRowCount(32); keyCombo.setSelectedIndex(0); // "none" - if (old != null && old.keyName != null && keyDevices.indexOf(old.keyName) != -1) - keyCombo.setSelectedIndex(keyDevices.indexOf(old.keyName)); + if (old != null && old.keyWrap != null && keyDevices.indexOf(old.keyWrap) != -1) + keyCombo.setSelectedIndex(keyDevices.indexOf(old.keyWrap)); + else if (findDevice(getLastTupleKey(synth), keyDevices) != null) + keyCombo.setSelectedItem(findDevice(getLastTupleKey(synth), keyDevices)); + + JTextField outID = null; + String initialID = synth.reviseID(null); + if (initialID != null) + outID = new JTextField(synth.reviseID(null)); JComboBox outChannelsCombo = new JComboBox(rc); + outChannelsCombo.setMaximumRowCount(17); if (old != null) outChannelsCombo.setSelectedIndex(old.outChannel - 1); + else if (getLastTupleOutChannel(synth) > 0) + outChannelsCombo.setSelectedIndex(getLastTupleOutChannel(synth) - 1); JComboBox keyChannelsCombo = new JComboBox(kc); + keyChannelsCombo.setMaximumRowCount(17); if (old != null) keyChannelsCombo.setSelectedIndex(old.keyChannel); + else if (getLastTupleKeyChannel(synth) > 0) + keyChannelsCombo.setSelectedIndex(getLastTupleKeyChannel(synth)); - boolean result = Synth.doMultiOption(root, new String[] { "Receive From", "Send To", "Send Channel", "Keyboard", "Keyboard Channel" }, new JComponent[] { inCombo, outCombo, outChannelsCombo, keyCombo, keyChannelsCombo }, "MIDI Devices", message); - + + boolean result = false; + synth.disableMenuBar(); + if (initialID != null) + result = Synth.showMultiOption(synth, new String[] { "Receive From", "Send To", "Send Channel", "Synth ID", "Controller", "Controller Channel" }, new JComponent[] { inCombo, outCombo, outChannelsCombo, outID, keyCombo, keyChannelsCombo }, "MIDI Devices", message); + else + result = Synth.showMultiOption(synth, new String[] { "Receive From", "Send To", "Send Channel", "Controller", "Controller Channel" }, new JComponent[] { inCombo, outCombo, outChannelsCombo, keyCombo, keyChannelsCombo }, "MIDI Devices", message); + synth.enableMenuBar(); + if (result) { // we need to build a tuple @@ -334,15 +485,30 @@ else if (outDevices.size() == 0) tuple.keyChannel = keyChannelsCombo.getSelectedIndex(); tuple.outChannel = outChannelsCombo.getSelectedIndex() + 1; + + if (initialID != null) + { + String prospectiveID = outID.getText(); + tuple.id = synth.reviseID(prospectiveID); + if (!tuple.id.equals(prospectiveID)) + { + synth.disableMenuBar(); + JOptionPane.showMessageDialog(synth, "The ID was revised to: " + tuple.id, "Device ID", JOptionPane.WARNING_MESSAGE); + synth.enableMenuBar(); + } + } tuple.inWrap = ((MidiDeviceWrapper)(inCombo.getSelectedItem())); tuple.in = tuple.inWrap.getThru(inReceiver); + tuple.inReceiver = inReceiver; if (tuple.in == null) { - JOptionPane.showOptionDialog(root, "An error occurred while connecting to the incoming MIDI Device.", + synth.disableMenuBar(); + JOptionPane.showOptionDialog(synth, "An error occurred while connecting to the incoming MIDI Device.", "Cannot Connect", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[] { "Run Disconnected" }, "Run Disconnected"); + synth.enableMenuBar(); return FAILED; } @@ -350,32 +516,47 @@ else if (outDevices.size() == 0) tuple.out = tuple.outWrap.getReceiver(); if (tuple.out == null) { - JOptionPane.showOptionDialog(root, "An error occurred while connecting to the outgoing MIDI Device.", + synth.disableMenuBar(); + JOptionPane.showOptionDialog(synth, "An error occurred while connecting to the outgoing MIDI Device.", "Cannot Connect", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[] { "Run Disconnected" }, "Run Disconnected"); + synth.enableMenuBar(); return FAILED; } if (keyCombo.getSelectedItem() instanceof String) { - tuple.keyName = null; + tuple.keyWrap = null; tuple.key = null; } else { - tuple.keyName = ((MidiDeviceWrapper)(keyCombo.getSelectedItem())); - tuple.key = tuple.keyName.getThru(keyReceiver); + tuple.keyWrap = ((MidiDeviceWrapper)(keyCombo.getSelectedItem())); + tuple.key = tuple.keyWrap.getThru(keyReceiver); + tuple.keyReceiver = keyReceiver; if (tuple.key == null) { - JOptionPane.showOptionDialog(root, "An error occurred while connecting to the Key Controller MIDI Device.", + synth.disableMenuBar(); + JOptionPane.showOptionDialog(synth, "An error occurred while connecting to the Controller MIDI Device.", "Cannot Connect", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, - new String[] { "Run without Keyboard Controller" }, "Run without Keyboard Controller"); - tuple.keyName = null; + new String[] { "Run without Controller" }, "Run without Controller"); + synth.enableMenuBar(); + tuple.keyWrap = null; tuple.key = null; } } + + setLastTupleIn(tuple.inWrap.toString(), synth); + setLastTupleOut(tuple.outWrap.toString(), synth); + if (tuple.keyWrap == null) + setLastTupleKey("None", synth); + else + setLastTupleKey(tuple.keyWrap.toString(), synth); + setLastTupleOutChannel(tuple.outChannel, synth); + setLastTupleKeyChannel(tuple.keyChannel, synth); + return tuple; } else @@ -384,4 +565,459 @@ else if (outDevices.size() == 0) } } } + + + public static final int CCDATA_TYPE_RAW_CC = 0; + public static final int CCDATA_TYPE_NRPN = 1; + public static final int CCDATA_TYPE_RPN = 2; + + + + + public static class CCData + { + public int type; + public int number; + public int value; + public int channel; + public boolean increment; + public CCData(int type, int number, int value, int channel, boolean increment) + { this.type = type; this.number = number; this.value = value; this.increment = increment; this.channel = channel; } + } + + + + + public static class Parser + { + + + ///// INTRODUCTION TO THE CC/RPN/NRPN PARSER + ///// The parser is located in handleGeneralControlChange(...), which + ///// can be set up to be the handler for CC messages by the MIDI library. + ///// + ///// CC messages take one of a great many forms, which we handle in the parser + ///// + ///// 7-bit CC messages: + ///// 1. number >=64 and < 96 or >= 102 and < 120, with value + ///// -> handleControlChange(channel, number, value, VALUE_7_BIT_ONLY) + ///// + ///// Potentially 7-bit CC messages, with MSB: + ///// 1. number >= 0 and < 32, other than 6, with value + ///// -> handleControlChange(channel, number, value * 128 + 0, VALUE_MSB_ONLY) + ///// + ///// Full 14-bit CC messages: + ///// 1. number >= 0 and < 32, other than 6, with MSB + ///// 2. same number + 32, with LSB + ///// -> handleControlChange(channel, number, MSB * 128 + LSB, VALUE) + ///// NOTE: this means that a 14-bit CC message will have TWO handleControlChange calls. + ///// There's not much we can do about this, as we simply don't know if the LSB will arrive. + ///// + ///// Continuing 14-bit CC messages: + ///// 1. number >= 32 and < 64, other than 38, with LSB, where number is 32 more than the last MSB. + ///// -> handleControlChange(channel, number, former MSB * 128 + LSB, VALUE) + ///// + ///// Lonely 14-bit CC messages (LSB only) + ///// 1. number >= 32 and < 64, other than 38, with LSB, where number is NOT 32 more than the last MSB. + ///// -> handleControlChange(channel, number, 0 + LSB, VALUE) + ///// + ///// + ///// NRPN Messages: + ///// All NRPN Messages start with: + ///// 1. number == 99, with MSB of NRPN parameter + ///// 2. number == 98, with LSB of NRPN parameter + ///// At this point NRPN MSB is set to 0 + ///// + ///// NRPN Messages then may have any sequence of: + ///// 3.1 number == 6, with value (MSB) + ///// -> handleNRPN(channel, parameter, value * 128 + 0, VALUE_MSB_ONLY) + ///// At this point we set the NRPN MSB + ///// 3.2 number == 38, with value (LSB) + ///// -> handleNRPN(channel, parameter, current NRPN MSB * 128 + value, VALUE_MSB_ONLY) + ///// 3.3 number == 96, with value (Increment) + ///// If value == 0 + ///// -> handleNRPN(channel, parameter, 1, INCREMENT) + ///// Else + ///// -> handleNRPN(channel, parameter, value, INCREMENT) + ///// Also reset current NRPN MSB to 0 + ///// 3.4 number == 97, with value + ///// If value == 0 + ///// -> handleNRPN(channel, parameter, 1, DECREMENT) + ///// Else + ///// -> handleNRPN(channel, parameter, value, DECREMENT) + ///// Also reset current NRPN MSB to 0 + ///// + ///// + ///// RPN Messages: + ///// All RPN Messages start with: + ///// 1. number == 99, with MSB of RPN parameter + ///// 2. number == 98, with LSB of RPN parameter + ///// At this point RPN MSB is set to 0 + ///// + ///// RPN Messages then may have any sequence of: + ///// 3.1 number == 6, with value (MSB) + ///// -> handleRPN(channel, parameter, value * 128 + 0, VALUE_MSB_ONLY) + ///// At this point we set the RPN MSB + ///// 3.2 number == 38, with value (LSB) + ///// -> handleRPN(channel, parameter, current RPN MSB * 128 + value, VALUE_MSB_ONLY) + ///// 3.3 number == 96, with value (Increment) + ///// If value == 0 + ///// -> handleRPN(channel, parameter, 1, INCREMENT) + ///// Else + ///// -> handleRPN(channel, parameter, value, INCREMENT) + ///// Also reset current RPN MSB to 0 + ///// 3.4 number == 97, with value + ///// If value == 0 + ///// -> handleRPN(channel, parameter, 1, DECREMENT) + ///// Else + ///// -> handleRPN(channel, parameter, value, DECREMENT) + ///// Also reset current RPN MSB to 0 + ///// + + ///// NULL messages: [RPN 127 with value of 127] + ///// 1. number == 101, value = 127 + ///// 2. number == 100, value = 127 + ///// [nothing happens, but parser resets] + ///// + ///// + ///// The big problem we have is that the MIDI spec allows a bare MSB or LSB to arrive and that's it! + ///// We don't know if another one is coming. If a bare LSB arrives we're supposed to assume the MSB is 0. + ///// But if the bare MSB comes we don't know if the LSB is next. So we either have to ignore it when it + ///// comes in (bad bad bad) or send two messages, one MSB-only and one MSB+LSB. + ///// This happens for CC, RPN, and NRPN. + ///// + ///// + ///// Our parser maintains four bytes in a struct called ControlParser: + ///// + ///// 0. status. This is one of: + ///// INVALID: the struct holds junk. CC: the struct is building a CC. + ///// RPN_START, RPN_END: the struct is building an RPN. + ///// NRPN_START, NRPN_END: the struct is building an NRPN. + ///// 1. controllerNumberMSB. In the low 7 bits. + ///// 2. controllerNumberLSB. In the low 7 bits. + ///// 3. controllerValueMSB. In the low 7 bits. This holds the previous MSB for potential "continuing" messages. + + // Parser status values + public static final int INVALID = 0; + public static final int NRPN_START = 1; + public static final int NRPN_END = 2; + public static final int RPN_START = 2; + public static final int RPN_END = 3; + + int[] status = new int[16]; // = INVALID; + + // The high bit of the controllerNumberMSB is either + // NEITHER_RPN_NOR_NRPN or it is RPN_OR_NRPN. + int[] controllerNumberMSB = new int[16]; + + // The high bit of the controllerNumberLSB is either + // RPN or it is NRPN + int[] controllerNumberLSB = new int[16]; + + // The controllerValueMSB[channel] is either a valid MSB or it is (-1). + int[] controllerValueMSB = new int[16]; + + // The controllerValueLSB is either a valid LSB or it is (-1). + int[] controllerValueLSB = new int[16]; + + + // we presume that the channel never changes + CCData parseCC(int channel, int number, int value, boolean requireLSB, boolean requireMSB) + { + // BEGIN PARSER + + // Start of NRPN + if (number == 99) + { + status[channel] = NRPN_START; + controllerNumberMSB[channel] = value; + return null; + } + + // End of NRPN + else if (number == 98) + { + controllerValueMSB[channel] = 0; + if (status[channel] == NRPN_START) + { + status[channel] = NRPN_END; + controllerNumberLSB[channel] = value; + controllerValueLSB[channel] = -1; + controllerValueMSB[channel] = -1; + } + else status[channel] = INVALID; + return null; + } + + // Start of RPN or NULL + else if (number == 101) + { + if (value == 127) // this is the NULL termination tradition, see for example http://www.philrees.co.uk/nrpnq.htm + { + status[channel] = INVALID; + } + else + { + status[channel] = RPN_START; + controllerNumberMSB[channel] = value; + } + return null; + } + + // End of RPN or NULL + else if (number == 100) + { + controllerValueMSB[channel] = 0; + if (value == 127) // this is the NULL termination tradition, see for example http://www.philrees.co.uk/nrpnq.htm + { + status[channel] = INVALID; + } + else if (status[channel] == RPN_START) + { + status[channel] = RPN_END; + controllerNumberLSB[channel] = value; + controllerValueLSB[channel] = -1; + controllerValueMSB[channel] = -1; + } + return null; + } + + else if ((number == 6 || number == 38 || number == 96 || number == 97) && (status[channel] == NRPN_END || status[channel] == RPN_END)) // we're currently parsing NRPN or RPN + { + int controllerNumber = (((int) controllerNumberMSB[channel]) << 7) | controllerNumberLSB[channel] ; + + if (number == 6) + { + controllerValueMSB[channel] = value; + if (requireLSB && controllerValueLSB[channel] == -1) + return null; + if (status[channel] == NRPN_END) + return handleNRPN(channel, controllerNumber, controllerValueLSB[channel] == -1 ? 0 : controllerValueLSB[channel], controllerValueMSB[channel]); + else + return handleRPN(channel, controllerNumber, controllerValueLSB[channel] == -1 ? 0 : controllerValueLSB[channel], controllerValueMSB[channel]); + } + + // Data Entry LSB for RPN, NRPN + else if (number == 38) + { + controllerValueLSB[channel] = value; + if (requireMSB && controllerValueMSB[channel] == -1) + return null; + if (status[channel] == NRPN_END) + return handleNRPN(channel, controllerNumber, controllerValueLSB[channel], controllerValueMSB[channel] == -1 ? 0 : controllerValueMSB[channel]); + else + return handleRPN(channel, controllerNumber, controllerValueLSB[channel], controllerValueMSB[channel] == -1 ? 0 : controllerValueMSB[channel]); + } + + // Data Increment for RPN, NRPN + else if (number == 96) + { + if (value == 0) + value = 1; + if (status[channel] == NRPN_END) + return handleNRPNIncrement(channel, controllerNumber, value); + else + return handleRPNIncrement(channel, controllerNumber, value); + } + + // Data Decrement for RPN, NRPN + else // if (number == 97) + { + if (value == 0) + value = -1; + if (status[channel] == NRPN_END) + return handleNRPNIncrement(channel, controllerNumber, -value); + else + return handleRPNIncrement(channel, controllerNumber, -value); + } + + } + + else // Some other CC + { + // status[channel] = INVALID; // I think it's fine to send other CC in the middle of NRPN or RPN + return handleRawCC(channel, number, value); + } + } + + public CCData processCC(ShortMessage message, boolean requireLSB, boolean requireMSB) + { + int num = message.getData1(); + int val = message.getData2(); + int channel = message.getChannel(); + return parseCC(channel, num, val, requireLSB, requireMSB); + } + + public CCData handleNRPN(int channel, int controllerNumber, int _controllerValueLSB, int _controllerValueMSB) + { + if (_controllerValueLSB < 0 || _controllerValueMSB < 0) + System.err.println("Warning (Midi): " + "LSB or MSB < 0. RPN: " + controllerNumber + " LSB: " + _controllerValueLSB + " MSB: " + _controllerValueMSB); + return new CCData(CCDATA_TYPE_NRPN, controllerNumber, _controllerValueLSB | (_controllerValueMSB << 7), channel, false); + } + + public CCData handleNRPNIncrement(int channel, int controllerNumber, int delta) + { + return new CCData(CCDATA_TYPE_NRPN, controllerNumber, delta, channel, true); + } + + public CCData handleRPN(int channel, int controllerNumber, int _controllerValueLSB, int _controllerValueMSB) + { + if (_controllerValueLSB < 0 || _controllerValueMSB < 0) + System.err.println("Warning (Midi): " + "LSB or MSB < 0. RPN: " + controllerNumber + " LSB: " + _controllerValueLSB + " MSB: " + _controllerValueMSB); + return new CCData(CCDATA_TYPE_RPN, controllerNumber, _controllerValueLSB | (_controllerValueMSB << 7), channel, false); + } + + public CCData handleRPNIncrement(int channel, int controllerNumber, int delta) + { + return new CCData(CCDATA_TYPE_RPN, controllerNumber, delta, channel, true); + } + + public CCData handleRawCC(int channel, int controllerNumber, int value) + { + return new CCData(CCDATA_TYPE_RAW_CC, controllerNumber, value, channel, false); + } + } + + public Parser controlParser = new Parser(); + public Parser synthParser = new Parser(); + + + /** A DividedSysex message is a Sysex MidiMessage which has been broken into chunks. + This allows us to send the Sysex as multiple messages, with pauses in-between each + message, so as not to overflow a MIDI buffer in machines such as a Kawai K1 whose + processors are not powerful enough to keep up with a large sysex dump. + +

The critical method is the factory method divide(), which creates the array + of divided sysex chunks. + */ + + public static class DividedSysex extends MidiMessage + { + public Object clone() + { + return new DividedSysex(getMessage()); + } + public int getStatus() { return 0xF0; } // not that this really matters + public DividedSysex(byte[] data) + { + super(data); + } + public static DividedSysex[] divide(MidiMessage sysex, int chunksize) + { + byte[] data = sysex.getMessage(); + int extra = 0; + if ((data.length / chunksize) * chunksize != data.length) + extra = 1; + DividedSysex[] m = new DividedSysex[data.length / chunksize + extra]; + for(int i = 0, chunk = 0; i < m.length; i++, chunk += chunksize) + { + byte[] d = new byte[Math.min(data.length - chunk, chunksize)]; + System.arraycopy(data, chunk, d, 0, d.length); + m[i] = new DividedSysex(d); + } + return m; + } + } + + public static String format(MidiMessage message) + { + if (message instanceof DividedSysex) + { + return "A Special Edisyn message (shouldn't happen)"; + } + else if (message instanceof MetaMessage) + { + return "A MIDI File MetaMessage (shouldn't happen)"; + } + else if (message instanceof SysexMessage) + { + return "Sysex (" + getManufacturerForSysex(((SysexMessage)message).getData()) + ")"; + } + else // ShortMessage + { + ShortMessage s = (ShortMessage) message; + int c = s.getChannel(); + String type = "Unknown"; + switch(s.getStatus()) + { + case ShortMessage.ACTIVE_SENSING: type = "Active Sensing"; c = -1; break; + case ShortMessage.CHANNEL_PRESSURE: type = "Channel Pressure"; break; + case ShortMessage.CONTINUE: type = "Continue"; c = -1; break; + case ShortMessage.CONTROL_CHANGE: type = "Control Change"; break; + case ShortMessage.END_OF_EXCLUSIVE: type = "End of Sysex Marker"; c = -1; break; + case ShortMessage.MIDI_TIME_CODE: type = "Midi Time Code"; c = -1; break; + case ShortMessage.NOTE_OFF: type = "Note Off"; break; + case ShortMessage.NOTE_ON: type = "Note On"; break; + case ShortMessage.PITCH_BEND: type = "Pitch Bend"; break; + case ShortMessage.POLY_PRESSURE: type = "Poly Pressure"; break; + case ShortMessage.PROGRAM_CHANGE: type = "Program Change"; break; + case ShortMessage.SONG_POSITION_POINTER: type = "Song Position Pointer"; c = -1; break; + case ShortMessage.SONG_SELECT: type = "Song Select"; c = -1; break; + case ShortMessage.START: type = "Start"; c = -1; break; + case ShortMessage.STOP: type = "Stop"; c = -1; break; + case ShortMessage.SYSTEM_RESET: type = "System Reset"; c = -1; break; + case ShortMessage.TIMING_CLOCK: type = "Timing Clock"; c = -1; break; + case ShortMessage.TUNE_REQUEST: type = "Tune Request"; c = -1; break; + } + return type + (c == -1 ? "" : (" (Channel " + c + ")")); + } + } + + + + static HashMap manufacturers = null; + + static HashMap getManufacturers() + { + if (manufacturers != null) + return manufacturers; + + manufacturers = new HashMap(); + Scanner scan = new Scanner(Midi.class.getResourceAsStream("Manufacturers.txt")); + while(scan.hasNextLine()) + { + String nextLine = scan.nextLine().trim(); + if (nextLine.equals("")) continue; + if (nextLine.startsWith("#")) continue; + + int id = 0; + Scanner scan2 = new Scanner(nextLine); + int one = scan2.nextInt(16); // in hex + if (one == 0x00) // there are two more to read + { + id = id + (scan2.nextInt(16) << 8) + (scan2.nextInt(16) << 16); + } + else + { + id = one; + } + manufacturers.put(new Integer(id), scan.nextLine().trim()); + } + return manufacturers; + } + + /** This works with or without F0 as the first data byte */ + public static String getManufacturerForSysex(byte[] data) + { + int offset = 0; + if (data[0] == (byte)0xF0) + offset = 1; + HashMap map = getManufacturers(); + if (data[0 + offset] == (byte)0x7D) // educational use + { + return (String)(map.get(new Integer(data[0 + offset]))) + + "\n\nNote that unregistered manufacturers or developers typically\n use this system exclusive region."; + } + else if (data[0 + offset] == (byte)0x00) + { + return (String)(map.get(new Integer( + 0x00 + + ((data[1 + offset] < 0 ? data[1 + offset] + 256 : data[1 + offset]) << 8) + + ((data[2 + offset] < 0 ? data[2 + offset] + 256 : data[2 + offset]) << 16)))); + } + else + { + return (String)(map.get(new Integer(data[0 + offset]))); + } + } } diff --git a/edisyn/Model.java b/edisyn/Model.java index d9fe1a31..f48ca88a 100644 --- a/edisyn/Model.java +++ b/edisyn/Model.java @@ -7,6 +7,7 @@ import java.util.*; import java.io.*; +import edisyn.gui.*; /** Storage for the various synthesizer parameters. The parameters are each associated @@ -19,40 +20,619 @@ with a KEY (a string), and presently may take either INTEGER or STRING values. interface. A listener can also be registered to be called whenever any value is updated. -

Mutation Sometimes you want to mutate or randomize parameters in some way. - The Model provides some support to indicate how or whether parameters should be - randomizable. Specifically, parameters can be declared IMMUTABLE, - meaning that they wish to resist being mutated or randomized; such parameters should - be handled specially (via the Synth.immutableMutate() method) rather than simply - randomized automatically by your code when the time comes. Integer Parameters can also - have a list of VALUES that are declared SPECIAL. This implies that these specific - values should be more often chosen through randomization than other values; one approach - is to (with 50% probability) choose one of those values, else choose any value in the range - between minimum and maximum inclusive. - -

Defaults You can also add a DEFAULT value for each parameter. This allows you - to reset all the parameters to default values, and to also print the parameters out which - deviate from those defaults. Note that the default value is not necessarily the same - thing as the 'default' value that you provide when retrieve a parameter -- that is simply - the value returned to indicate that no parameter existed in the model. +

Mutation. To assist in mutating or crossing over parameters, the model @author Sean Luke */ -public class Model +public class Model implements Cloneable { LinkedHashMap storage = new LinkedHashMap(); HashMap min = new HashMap(); HashMap max = new HashMap(); HashMap listeners = new HashMap(); - HashSet immutable = new HashSet(); - HashMap special = new HashMap(); - HashMap defaults = new HashMap(); + HashMap metricMin = new HashMap(); + HashMap metricMax = new HashMap(); + HashMap validMin = new HashMap(); + HashMap validMax = new HashMap(); + HashMap status = new HashMap(); + Undo undoListener = null; String lastKey = null; public static final String ALL_KEYS = "ALL_KEYS"; + public Undo getUndoListener() { return undoListener; } + public void setUndoListener(Undo up) { undoListener = up; } + + /** Returns TRUE with the given probability. */ + boolean coinToss(Random random, double probability) + { + if (probability==0.0) return false; // fix half-open issues + else if (probability==1.0) return true; // fix half-open issues + else return random.nextDouble() < probability; + } + + /** Produces a random value in the fully closed range [a, b]. */ + static int randomValueWithin(Random random, int a, int b) + { + if (a > b) { int swap = a; a = b; b = swap; } + if (a == b) return a; + int range = (b - a + 1); + return a + random.nextInt(range); + } + + public final static int VALID_RETRIES = 20; + /** Produces a random valid value in the fully closed range [a, b]. */ + int randomValidValueWithin(String key, Random random, int a, int b) + { + for(int i = 0; i < VALID_RETRIES; i++) + { + int v = randomValueWithin(random, a, b); + if (isValid(key, v)) + return v; + } + return get(key, 0); // return original + } + +/* + import edisyn.*; + m = new Model(); + r = new Random(1000); + show(); + for(int i = 0; i < 10000; i++) m.randomValueWithin(r, 5, 10, 7, 0.05); +*/ + + static final double STDDEV_CUT = 1.0/2.0; + + public static int randomValueWithin(Random random, int a, int b, int center, double weight) + { + if (a > b) { int swap = a; a = b; b = swap; } + if (a == b) return a; + if (weight == 0) + return center; + else if (weight == 1) + return randomValueWithin(random, a, b); + else + { + double stddev = (1.0 / (1.0 - weight)) - 1.0; + double delta = 0.0; + + while(true) + { + double rand = (random.nextGaussian() * stddev * STDDEV_CUT) % 2.0; + delta = rand * (b - a); + if ((center + delta) > a - 0.5 && + (center + delta) < b + 0.5) + break; + } + return (int)(Math.round(center + delta)); + } + } + + /** Produces a random value in the fully closed range [a, b], + choosing from a uniform distribution of size +- 2 * weight * (b-a+1), + centered at *center*, and rounded to the nearest integer. */ + public int randomValueWithin2(Random random, int a, int b, int center, double weight) + { + if (a > b) { int swap = a; a = b; b = swap; } + if (a == b) return a; + + double range = b - a + 1; // 0.5 extra on each side + + // pick a random number from -1...+1 + double delta = 0.0; + while(true) + { + delta = Math.ceil((random.nextDouble() * 2 - 1) * weight * range); + if ((center + delta) >= a && + (center + delta) <= b) + break; + } + return (int)(Math.round(center + delta)); + } + + /** Produces a random valid value in the fully closed range [a, b], + choosing from a uniform distribution of size +- weight * (a-b), + centered at *center*, and rounded to the nearest integer. */ + int randomValidValueWithin(String key, Random random, int a, int b, int center, double weight) + { + for(int i = 0; i < VALID_RETRIES; i++) + { + int v = randomValueWithin(random, a, b, center, weight); + if (isValid(key, v)) + return v; + } + return get(key, 0); // return original + } + + + /** Mutates (potentially) all keys. + Mutation works as follows. For each key, we first see if we're permitted to mutate it + (no immutable status, no strings). If so, we divide the range into the METRIC and NON-METRIC + regions. If it's all NON-METRIC, then with WEIGHT probability we will pick a new + value at random (else stay). Else if we're in a non-metric region, with 0.5 chance we'll + pick a new random non-metric value with WEIGHT probability (else stay), and with 0.5 chance we'll + pick a completely random metric value with WEIGHT probability (else stay). Else if we're in a metric + region, with 0.5 chance we will pick a non-metric value with WEIGHT probability (else stay), and with + 0.5 chance we will do a METRIC MUTATION. + +

A metric mutation selects under a uniform rectangular distribution centered at the current value. + The rectangular distribution is the delta function when WEIGHT is 0.0 and is the full range from + metric min to metric max inclusive when WEIGHT is 1.0. We repeat this selection until we get a value + within metric min and metric max. + */ + public Model mutate(Random random, double weight) + { + return mutate(random, getKeys(), weight); + } + + /** Mutates (potentially) the keys provided. + Mutation works as follows. For each key, we first see if we're permitted to mutate it + (no immutable status, no strings). If so, we divide the range into the METRIC and NON-METRIC + regions. If it's all NON-METRIC, then with WEIGHT probability we will pick a new + value at random (else stay). Else if we're in a non-metric region, with 0.5 chance we'll + pick a new random non-metric value with WEIGHT probability (else stay), and with 0.5 chance we'll + pick a completely random metric value with WEIGHT probability (else stay). Else if we're in a metric + region, with 0.5 chance we will pick a non-metric value with WEIGHT probability (else stay), and with + 0.5 chance we will do a METRIC MUTATION. + +

A metric mutation selects under a uniform rectangular distribution centered at the current value. + The rectangular distribution is the delta function when WEIGHT is 0.0 and is the full range from + metric min to metric max inclusive when WEIGHT is 1.0. We repeat this selection until we get a value + within metric min and metric max. + */ + public Model mutate(Random random, String[] keys, double weight) + { + if (undoListener!= null) + { + undoListener.push(this); + undoListener.setWillPush(false); + } + + for(int i = 0; i < keys.length; i++) + { + if (!exists(keys[i])) { continue; } + // continue if the key is immutable, it's a string, or we fail the coin toss + if (getStatus(keys[i]) == STATUS_IMMUTABLE || getStatus(keys[i]) == STATUS_RESTRICTED || isString(keys[i])) continue; + if (minExists(keys[i]) && maxExists(keys[i]) && getMin(keys[i]) >= getMax(keys[i])) continue; // no range + + boolean hasMetric = false; // do we even HAVE a metric range? + boolean doMetric = false; // are we in that range, and should mutate within it? + boolean pickRandomInMetric = false; // are we NOT in that range, but should maybe go to a random value in it? + + if (metricMinExists(keys[i]) && + metricMaxExists(keys[i])) + { + hasMetric = true; + if (getMetricMax(keys[i]) == getMax(keys[i]) && + getMetricMin(keys[i]) == getMin(keys[i])) // has no non-metric + { + doMetric = true; + } + else if (get(keys[i], 0) >= getMetricMin(keys[i]) && + get(keys[i], 0) <= getMetricMax(keys[i])) // we're within metric range + { + if (coinToss(random, 0.5)) + doMetric = true; // we will stay in the metric range and mutate within it (versus jump out) + } + else // we're outside the metric range + { + if (coinToss(random, 0.5)) + pickRandomInMetric = true; // we are out of the metric range but may go inside it (versus stay outside) + } + } + else // there is no metric range + { + // do nothing. + } + + // now perform the operation + + if (doMetric) // definitely do a metric mutation + { + int a = getMetricMin(keys[i]); + int b = getMetricMax(keys[i]); + double mutWeight = weight; + double mutProb = mutWeight; + if (random.nextDouble() < mutProb) + { + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), + randomValidValueWithin(keys[i], random, getMetricMin(keys[i]), getMetricMax(keys[i]), get(keys[i], 0), mutWeight))); + } + } + else if (pickRandomInMetric) // MAYBE jump into metric + { + if (coinToss(random, weight)) + { + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), + randomValidValueWithin(keys[i], random, getMetricMin(keys[i]), getMetricMax(keys[i])))); + } + } + else if (hasMetric) // MAYBE choose a random new non-metric location + { + if (coinToss(random, weight)) + { + for(int x = 0; x < VALID_RETRIES; i++) + { + int lowerRange = getMetricMin(keys[i]) - getMin(keys[i]); + int upperRange = getMax(keys[i]) - getMetricMax(keys[i]); + int delta = random.nextInt(lowerRange + upperRange); + if (delta < lowerRange) + { + if (isValid(keys[i], getMin(keys[i]) + delta)) + { + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), getMin(keys[i]) + delta)); + break; + } + } + else + { + if (isValid(keys[i], getMetricMax(keys[i]) + 1 + (delta - lowerRange))) + { + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), getMetricMax(keys[i]) + 1 + (delta - lowerRange))); + break; + } + } + } + } + } + else // MAYBE choose a random new non-metric location (easiest because there is no metric location) + { + if (coinToss(random, weight)) + { + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), + randomValidValueWithin(keys[i], random, getMin(keys[i]), getMax(keys[i])))); + } + } + } + + if (undoListener!= null) + { + undoListener.setWillPush(true); + } + return this; + } + + void setIfValid(String key, int value) + { + if (isValid(key, value)) + set(key, value); + else + System.err.println("Warning (Model): " + "Invalid opposite value for " + key + ": " + value); + } + + /** Finds a point on the OPPOSITE side of the model from where the provided other MODEL is located. + Let's call the current model X and the provided model Y. + Changes all (and only) the METRIC parameters for which both X and Y are currently in metric regions. + This is done by identifying the value of the parameter on the OPPOSITE side of X from where Y is. + We reduce this value Z (move it closer to X's value) by WEIGHT, bound Z to be within the metric + region, and then choose a new random value between X and Z inclusive. + */ + public Model opposite(Random random, Model model, double weight) + { + return opposite(random, model, getKeys(), weight, false); + } + + /** Finds a point on the OPPOSITE side of the model from where the provided other MODEL is located, + for the given keys. Let's call the current model X and the provided model Y. + Changes all (and only) the METRIC parameters for which both X and Y are currently in metric regions. + This is done by identifying the value of the parameter on the OPPOSITE side of X from where Y is. + We reduce this value Z (move it closer to X's value) by WEIGHT, bound Z to be within the metric + region, and then choose a new random value between X and Z inclusive. + */ + public Model opposite(Random random, Model model, String[] keys, double weight, boolean fleeIfSame) + { + if (undoListener!= null) + { + undoListener.push(this); + undoListener.setWillPush(false); + } + + for(int i = 0; i < keys.length; i++) + { + // return if the key doesn't exist, is immutable or is a string, or is non-metric for someone + if (!model.exists(keys[i])) { continue; } + if (getStatus(keys[i]) == STATUS_IMMUTABLE || getStatus(keys[i]) == STATUS_RESTRICTED || isString(keys[i])) continue; + if (minExists(keys[i]) && maxExists(keys[i]) && getMin(keys[i]) >= getMax(keys[i])) continue; // no range + + if ((get(keys[i], 0) == model.get(keys[i])) && fleeIfSame) + { + // need to flee. First: are we metric? + if (metricMinExists(keys[i]) && + metricMaxExists(keys[i]) && + get(keys[i], 0) >= getMetricMin(keys[i]) && + get(keys[i], 0) <= getMetricMax(keys[i])) + { + // since we're the same, the best we can do here is do a LITTLE + // mutation (mutating by 1) while staying metric, so that NEXT + // time if we continue to flee, we'll keep on fleeing in that + // direction + if (getMetricMin(keys[i]) == getMetricMax(keys[i])) // uh oh + { } // don't set anything + else if (get(keys[i], 0) == getMetricMax(keys[i])) + setIfValid(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), + get(keys[i], 0) - 1)); + else if (get(keys[i], 0) == getMetricMin(keys[i])) + setIfValid(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), + get(keys[i], 0) + 1)); + else + setIfValid(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), + get(keys[i], 0) + (random.nextBoolean() ? 1 : -1))); + } + else + { + if (coinToss(random, weight)) + { + int val = 0; + for(int j = 0; j < 10; j++) // we'll try ten times to find something new + { + val = randomValidValueWithin(keys[i], random, getMin(keys[i]), getMax(keys[i])); + if (val != get(keys[i], 0)) // we want to be different + break; + } + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), val)); + } + } + } + else if (metricMinExists(keys[i]) && + metricMaxExists(keys[i]) && + get(keys[i], 0) >= getMetricMin(keys[i]) && + get(keys[i], 0) <= getMetricMax(keys[i]) && + model.get(keys[i], 0) >= getMetricMin(keys[i]) && + model.get(keys[i], 0) <= getMetricMax(keys[i])) + { + // different but both metric. + + int a = get(keys[i], 0); + int b = model.get(keys[i], a); + + // determine range + double qq = a + weight * (a - b); + int q = 0; + + // round away from b + if (b > a) + q = (int)Math.floor(qq); + else + q = (int)Math.ceil(qq); + + // bound + if (metricMinExists(keys[i]) && q < getMetricMin(keys[i])) + q = getMetricMin(keys[i]); + if (metricMaxExists(keys[i]) && q > getMetricMax(keys[i])) + q = getMetricMax(keys[i]); + + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), randomValidValueWithin(keys[i], random, a, q))); + } + else + { + // different but someone is non-metric. Don't change. + continue; + } + } + + if (undoListener!= null) + { + undoListener.setWillPush(true); + } + + return this; + } + + + + + /** Recombines (potentially) all keys. + Recombination works as follows. For each key, we first see if we're permitted to mutate it + (no immutable status, other model doesn't have the key). Next with 1.0 - WEIGHT probability + we don't recombine at all. Otherwise we recombine: + +

If the parameter is a string, we keep our value. + If the parameter is an integer, and we have a metric range, + and BOTH our value AND the other model's value are within that range, then we do + metric crossover: we pick a random new value between the two values inclusive. + Otherwise with 0.5 probability we select our parameter, else the other model's parameter. + */ + public Model recombine(Random random, Model model, double weight) + { + return recombine(random, model, getKeys(), weight); + } + + /** Recombines (potentially the keys provided. + Recombination works as follows. For each key, we first see if we're permitted to mutate it + (no immutable status, other model doesn't have the key). Next with 1.0 - WEIGHT probability + we don't recombine at all. Otherwise we recombine: + +

If the parameter is a string, we keep our value. + If the parameter is an integer, and we have a metric range, + and BOTH our value AND the other model's value are within that range, then we do + metric crossover: we pick a random new value between the two values inclusive. + Otherwise with 0.5 probability we select our parameter, else the other model's parameter. + */ + public Model recombine(Random random, Model model, String[] keys, double weight) + { + if (undoListener!= null) + { + undoListener.push(this); + undoListener.setWillPush(false); + } + + for(int i = 0; i < keys.length; i++) + { + // skip if the key doesn't exist, is immutable, is restricted, or is a string + if (!model.exists(keys[i])) { continue; } + if (getStatus(keys[i]) == STATUS_IMMUTABLE || isString(keys[i]) || getStatus(keys[i]) == STATUS_RESTRICTED) continue; + if (minExists(keys[i]) && maxExists(keys[i]) && getMin(keys[i]) >= getMax(keys[i])) continue; // no range + + // we cross over metrically if we're both within the metric range + if (metricMinExists(keys[i]) && + metricMaxExists(keys[i]) && + get(keys[i], 0) >= getMetricMin(keys[i]) && + get(keys[i], 0) <= getMetricMax(keys[i]) && + model.get(keys[i], 0) >= getMetricMin(keys[i]) && + model.get(keys[i], 0) <= getMetricMax(keys[i])) + { + int a = get(keys[i], 0); + int b = model.get(keys[i], a); + double qq = a - weight * (a - b); + + int q = 0; + + // round towards b + if (b > a) + q = (int)Math.ceil(qq); + else + q = (int)Math.floor(qq); + + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), randomValidValueWithin(keys[i], random, a, q))); + } + else if (coinToss(random, weight)) + { + if (coinToss(random, 0.5)) + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), model.get(keys[i], 0))); + } + } + + if (undoListener!= null) + { + undoListener.setWillPush(true); + } + return this; + } + + + /** Crosses over the keys provided. This works as follows. + For each key, we first see if we're permitted to mutate it + (no immutable status, other model doesn't have the key). Next with 1.0 - WEIGHT probability + we don't cross over at all. Otherwise we adopt the parameter from the other individual half of the time. + */ + public Model crossover(Random random, Model model, String[] keys, double weight) + { + if (undoListener!= null) + { + undoListener.push(this); + undoListener.setWillPush(false); + } + + for(int i = 0; i < keys.length; i++) + { + // skip if the key doesn't exist, is immutable, is restricted, or is a string + if (!model.exists(keys[i])) { continue; } + if (getStatus(keys[i]) == STATUS_IMMUTABLE || isString(keys[i]) || getStatus(keys[i]) == STATUS_RESTRICTED) continue; + + if (coinToss(random, weight)) + { + if (coinToss(random, 0.5)) + set(keys[i], reviseMutatedValue(keys[i], get(keys[i], 0), model.get(keys[i], 0))); + } + } + + if (undoListener!= null) + { + undoListener.setWillPush(true); + } + + return this; + } + + + public void clearListeners() + { + listeners = new HashMap(); + undoListener = null; + } + + HashMap getCopy(HashMap map) + { + HashMap m = new HashMap(); + m.putAll(map); + return m; + } + + public Object clone() + { + Model model = null; + try { model = (Model)(super.clone()); } + catch (Exception e) { e.printStackTrace(); } // never happens + + // we do putAll getCopy rather than + model.storage = new LinkedHashMap(); + model.storage.putAll(storage); + model.min = getCopy(min); + model.max = getCopy(max); + model.listeners = getCopy(listeners); + model.status = getCopy(status); + model.metricMin = getCopy(metricMin); + model.metricMax = getCopy(metricMax); + model.validMin = getCopy(validMin); + model.validMax = getCopy(validMax); + model.lastKey = null; + return model; + } + + public boolean equals(Object other) + { + if (other == null || !(other instanceof Model)) + return false; + Model model = (Model) other; + if (!storage.equals(model.storage)) + return false; + if (!min.equals(model.min)) + return false; + if (!max.equals(model.max)) + return false; + if (!listeners.equals(model.listeners)) + return false; + if (!status.equals(model.status)) + return false; + if (!metricMin.equals(model.metricMin)) + return false; + if (!metricMax.equals(model.metricMax)) + return false; + if (!validMin.equals(model.validMin)) + return false; + if (!validMax.equals(model.validMax)) + return false; + // don't care about lastKey + return true; + } + + + public boolean keyEquals(Model other) + { + if (other == null) + return false; + if (!storage.equals(other.storage)) + return false; + return true; + } + + public void updateAllListeners() + { + String[] keys = getKeys(); + for(int i = 0; i < keys.length; i++) + { + updateListenersForKey(keys[i]); + } + } + + /** Does a clone except for the various listeners */ + public Model copy() + { + Model m =((Model)clone()); + m.clearListeners(); + return m; + } + + public void copyValuesTo(Model model) + { + model.storage.clear(); + model.storage.putAll(storage); + model.updateAllListeners(); + model.lastKey = null; + } + /** Register a listener to be notified whenever the value associated with the given key is updated. If the key is ALL_KEYS, then the listener will be notified whenever any key is updated. */ @@ -65,71 +645,82 @@ public void register(String key, Updatable component) listeners.put(key, list); } - /** Returns all the keys in the model as an array. */ + /** Returns all the keys in the model as an array, except the hidden ones. */ public String[] getKeys() { - return (String[])(storage.keySet().toArray(new String[0])); - } - - public String getLastKey() - { - return lastKey; - } - - /** Add the given integer as a default for the key. */ - public void addDefault(String key, int value) - { - defaults.put(key, Integer.valueOf(value)); - } - - /** Add the given String as a default for the key. */ - public void addDefault(String key, String value) - { - defaults.put(key, value); + String[] keyset = (String[])(storage.keySet().toArray(new String[0])); + ArrayList revisedKeys = new ArrayList(); + for(int i = 0; i < keyset.length; i++) + if (getStatus(keyset[i]) != STATUS_RESTRICTED) + revisedKeys.add(keyset[i]); + return (String[])(revisedKeys.toArray(new String[0])); } - /** Return the given default for the key (as an Integer or as a String), or null if there is none. */ - public Object getDefault(String key) + public void clearLastKey() { - return defaults.get(key); + lastKey = null; } - /** Return whether a default has been entered for the given key. */ - public boolean defaultExists(String key) + public String getLastKey() { - return (defaults.containsKey(key)); + return lastKey; } - + + public static final boolean debug = false; + /** Adds a key with the given Integer value, or changes it to the given value. */ public void set(String key, int value) { - if (key.equals("name")) - new Throwable().printStackTrace(); + if (debug) + { + System.err.println("Debug (Model):" + key + " --> " + value + " [" + getMin(key) + " - " + getMax(key) + "]" ); + if (!exists(key)) + System.err.println("Debug (Model): " + "Key " + key + " was NEW"); + } + // when do we push on the undo stack? + if ( + undoListener != null && // when we have an undo listener AND + !key.equals(lastKey) && // when the key is not the lastKey AND + (!exists(key) || // the key doesn't exist OR + !isInteger(key) || // the value isn't an integer OR + value != get(key, 0))) // the value doesn't match the current value + undoListener.push(this); storage.put(key, Integer.valueOf(value)); - ArrayList list = (ArrayList)(listeners.get(key)); - if (list != null) + lastKey = key; + updateListenersForKey(key); + } + + + public void setBounded(String key, int value) + { + if (isString(key)) + return; + + if (minExists(key)) { - for(int i = 0; i < list.size(); i++) - { - ((Updatable)(list.get(i))).update(key, this); - } + int min = getMin(key); + if (value < min) + value = min; } - list = (ArrayList)(listeners.get(ALL_KEYS)); - if (list != null) + + if (maxExists(key)) { - for(int i = 0; i < list.size(); i++) - { - ((Updatable)(list.get(i))).update(key, this); - } + int max = getMax(key); + if (value > max) + value = max; } - lastKey = key; - } - - /** Adds a key with the given String value, or changes it to the given value. */ - public void set(String key, String value) + set(key, value); + } + + boolean updateListeners = true; + public void setUpdateListeners(boolean val) { updateListeners = val; } + public boolean getUpdateListeners() { return updateListeners; } + + public void updateListenersForKey(String key) { - storage.put(key, value); + if (!updateListeners) return; + ArrayList list = (ArrayList)(listeners.get(key)); if (list != null) { @@ -146,57 +737,40 @@ public void set(String key, String value) ((Updatable)(list.get(i))).update(key, this); } } - lastKey = key; } - - public void resetToDefaults() + + /** Adds a key with the given String value, or changes it to the given value. */ + public void set(String key, String value) { - String[] keys = getKeys(); - for(int i = 0; i < keys.length; i++) + if (debug) { - if (defaultExists(keys[i])) - { - if (isString(keys[i])) - { - set(keys[i], (String)getDefault(keys[i])); - } - else - { - set(keys[i], ((Integer)getDefault(keys[i])).intValue()); - } - } + if (debug) System.err.println("Debug (Model): " + key + " --> " + value); + if (!exists(key)) + System.err.println("Debug (Model): " + "Key " + key + " was NEW"); } - lastKey = null; - } - - /** Returns an array of integer values associated with this - (Integer) key which have been declared SPECIAL, meaning that they - should be more commonly mutated to than other values. */ - public int[] getSpecial(String key) - { - return (int[])special.get(key); - } - - /** Sets an array of integer values associated with this - (Integer) key which have been declared SPECIAL, meaning that they - should be more commonly mutated to than other values. */ - public void setSpecial(String key, int[] stuff) - { - special.put(key, stuff); - } - - /** Sets a single value associated with this - (Integer) key which has been declared SPECIAL, meaning that they - it be more commonly mutated to than other values. */ - public void setSpecial(String key, int stuff) - { - special.put(key, new int[] { stuff }); - } + // when do we push on the undo stack? + if ( + undoListener != null && // when we have an undo listener AND + !key.equals(lastKey) && // when the key is not the lastKey AND + (!exists(key) || // the key doesn't exist OR + !isString(key) || // the value isn't a string OR + !value.equals(get(key, null)))) // the value doesn't match the current value + undoListener.push(this); + storage.put(key, value); + lastKey = key; + updateListenersForKey(key); + } + /** Returns the value associated with this (String) key, or ifDoesntExist if there is no such value. */ public String get(String key, String ifDoesntExist) { + if (debug) + { + if (!exists(key)) + System.err.println("Debug (Model): " + "Key " + key + " does not exist"); + } String d = (String) (storage.get(key)); if (d == null) return ifDoesntExist; else return d; @@ -206,13 +780,35 @@ public String get(String key, String ifDoesntExist) (Integer) key, or ifDoesntExist if there is no such value. */ public int get(String key, int ifDoesntExist) { + if (debug) + { + if (!exists(key)) + System.err.println("Debug (Model): " + "Key " + key + " does not exist"); + } Integer d = (Integer) (storage.get(key)); if (d == null) return ifDoesntExist; else return d.intValue(); } + + /** Returns the value associated with this (Integer) key, or -1 if there is no such value. + If there is no such value, also prints (does not throw) a RuntimeError stacktrace. */ + public int get(String key) + { + if (debug) + { + if (!exists(key)) + System.err.println("Debug (Model): " + "Key " + key + " does not exist"); + } + Integer d = (Integer) (storage.get(key)); + if (d == null) + { + new RuntimeException("Debug (Model): " + "No Value stored for key " + key + ", returning -1, which is certainly wrong.").printStackTrace(); + return -1; + } + else return d.intValue(); + } - - public Object get(String key) { return storage.get(key); } + Object getValue(String key) { return storage.get(key); } /** Returns whether the key is associated with a String. If there is no key stored in the Model, then FALSE is returned. */ @@ -223,6 +819,17 @@ public boolean isString(String key) else return (storage.get(key) instanceof String); } + + /** Returns whether the key is associated with an integer. + If there is no key stored in the Model, then FALSE is returned. */ + public boolean isInteger(String key) + { + if (!exists(key)) + return false; + else return (storage.get(key) instanceof Integer); + } + + /** Returns whether the key is stored in the model. */ public boolean exists(String key) { @@ -253,26 +860,90 @@ public void setMax(String key, int value) max.put(key, Integer.valueOf(value)); } - /** Sets whether a given key is declared immutable. */ - public void setImmutable(String key, boolean val) + /** Returns whether a metric minimum is stored in the model for the key. */ + public boolean metricMinExists(String key) { - if (val) - immutable.add(key); - else - immutable.remove(key); + return metricMin.containsKey(key); + } + + /** Returns whether a metric maximum is stored in the model for the key. */ + public boolean metricMaxExists(String key) + { + return metricMax.containsKey(key); + } + + /** Returns whether an valid minimum is stored in the model for the key. */ + public boolean validMinExists(String key) + { + return validMin.containsKey(key); + } + + /** Returns whether an valid maximum is stored in the model for the key. */ + public boolean validMaxExists(String key) + { + return validMax.containsKey(key); + } + + /** Sets the metric minimum for a given key. */ + public void setMetricMin(String key, int value) + { + metricMin.put(key, Integer.valueOf(value)); } - /** Returns whether a given key is declared immutable. */ - public boolean isImmutable(String key) + /** Sets the metric maximum for a given key. */ + public void setMetricMax(String key, int value) + { + metricMax.put(key, Integer.valueOf(value)); + } + + /** Sets the valid minimum for a given key. */ + public void setValidMin(String key, int value) + { + validMin.put(key, Integer.valueOf(value)); + } + + /** Sets the valid maximum for a given key. */ + public void setValidMax(String key, int value) + { + validMax.put(key, Integer.valueOf(value)); + } + + public static final int STATUS_FREE = 0; + public static final int STATUS_IMMUTABLE = 1; + public static final int STATUS_RESTRICTED = 2; + + /** Sets the status of a key. The default is STATUS_FREE, except for strings, which are STATUS_IMMUTABLE. */ + public void setStatus(String key, int val) { - return immutable.contains(key); + status.put(key, Integer.valueOf(val)); + } + + /** Returns whether a given key is declared immutable. Strings are ALWAYS immutable and you don't need to set them. */ + public int getStatus(String key) + { + if (status.containsKey(key)) + { + return ((Integer)(status.get(key))).intValue(); + } + else if (!exists(key)) + { + return STATUS_IMMUTABLE; + } + else if (isString(key)) + { + return STATUS_IMMUTABLE; + } + else // it's a number + { + return STATUS_FREE; + } } /** Returns the minimum for a given key, or 0 if no minimum is declared. */ public int getMin(String key) { Integer d = (Integer) (min.get(key)); - if (d == null) { System.err.println("Nonexistent min extracted for " + key); return 0; } + if (d == null) { System.err.println("Warning (Model): " + "Nonexistent min extracted for " + key); return 0; } else return d.intValue(); } @@ -280,40 +951,190 @@ public int getMin(String key) public int getMax(String key) { Integer d = (Integer) (max.get(key)); - if (d == null) { System.err.println("Nonexistent max extracted for " + key); return 0; } + if (d == null) { System.err.println("Warning (Model): " + "Nonexistent max extracted for " + key); return 0; } else return d.intValue(); } - /** Print all the model parameters to stderr. */ - public void print() + /** Returns the metric minimum for a given key, or 0 if no minimum is declared. */ + public int getMetricMin(String key) { - PrintWriter pw = new PrintWriter(System.err); - print(pw, false); - pw.flush(); + Integer d = (Integer) (metricMin.get(key)); + if (d == null) { System.err.println("Warning (Model): " + "Nonexistent metricMin extracted for " + key); return 0; } + else return d.intValue(); } - /** Print the model parameters to the given writer. If diffsOnly, then only the model parameters which - differ from the default will be printed. */ - public void print(PrintWriter out, boolean diffsOnly) + /** Returns the metric maximum for a given key, or 0 if no maximum is declared. */ + public int getMetricMax(String key) + { + Integer d = (Integer) (metricMax.get(key)); + if (d == null) { System.err.println("Warning (Model): " + "Nonexistent metricMax extracted for " + key); return 0; } + else return d.intValue(); + } + + /** Returns the valid minimum for a given key, or 0 if no minimum is declared. */ + public int getValidMin(String key) + { + Integer d = (Integer) (validMin.get(key)); + if (d == null) { System.err.println("Warning (Model): " + "Nonexistent validMin extracted for " + key); return 0; } + else return d.intValue(); + } + + /** Returns the valid maximum for a given key, or 0 if no maximum is declared. */ + public int getValidMax(String key) + { + Integer d = (Integer) (validMax.get(key)); + if (d == null) { System.err.println("Warning (Model): " + "Nonexistent validMax extracted for " + key); return 0; } + else return d.intValue(); + } + + public boolean isValid(String key, int val) + { + boolean hasValidMin = validMinExists(key); + boolean hasValidMax = validMaxExists(key); + + // if we have no restrictions, then everything is valid + if (!hasValidMin && !hasValidMax) + return true; + // if we have one restriction... + else if (!hasValidMin) + return (val <= getValidMax(key)); + else if (!hasValidMax) + return (val >= getValidMin(key)); + // we have two restrictions + else + return (val >= getValidMin(key) && val <= getValidMax(key)); + } + + /** Deletes the metric min and max for a key */ + public void removeMetricMinMax(String key) + { + metricMin.remove(key); + metricMax.remove(key); + } + + public int getRange(String key) + { + if (minExists(key) && maxExists(key)) + { + return getMax(key) - getMin(key) + 1; + } + else return 0; + } + + /** Print to stderr those model parameters for which the provided "other" model + does not have identical values. + */ + public void printDiffs(Model other) + { + printDiffs(new PrintWriter(new OutputStreamWriter(System.err)), other); + } + + + /** Print to the given writer those model parameters for which the provided "other" model + does not have identical values. + */ + public void printDiffs(PrintWriter out, Model other) { String[] keys = getKeys(); for(int i = 0; i < keys.length; i++) { if (isString(keys[i])) { - String str = get(keys[i], ""); - if (diffsOnly && str.equals(getDefault(keys[i]))) + if (other.isString(keys[i]) && + other.getValue(keys[i]).equals(getValue(keys[i]))) // they're the same continue; + String str = get(keys[i], ""); out.println(keys[i] + ": \"" + get(keys[i], "") + "\" "); } - else + else if (isInteger(keys[i])) { - int j = get(keys[i], 0); - if (diffsOnly && getDefault(keys[i]) != null && - j == ((Integer)(getDefault(keys[i]))).intValue()) + if (other.isInteger(keys[i]) && + other.getValue(keys[i]).equals(getValue(keys[i]))) // they're the same continue; + int j = get(keys[i], 0); out.println(keys[i] + ": " + j + " "); } + else + { + out.println(keys[i] + ": FOREIGN OBJECT " + get(keys[i])); + } + } + } + + /** Print the model parameters to stderr. If diffsOnly, then only the model parameters which + differ from the default will be printed. */ + public void print() + { + print(new PrintWriter(new OutputStreamWriter(System.err))); + } + + static final String FALSE_STRING = "<<<>>>>"; + static final String TRUE_STRING = "<<<>>>>"; + String getModelParameterText(String key) + { + if (isString(key)) + return "\"" + get(key, "") + "\""; + + ArrayList l = (ArrayList)(listeners.get(key)); + if (l == null) + return "" + get(key, 0); + + for(int i = 0; i < l.size(); i++) + { + Object obj = l.get(i); + // Lots of things can be NumericalComponents so we're just going + // to focus on the primary items + if (obj instanceof Chooser) + { + return ((Chooser)obj).map(get(key, 0)); + } + else if (obj instanceof CheckBox) + { + return (get(key, 0) == 0 ? FALSE_STRING : TRUE_STRING); + } + else if (obj instanceof LabelledDial) + { + return ((LabelledDial)obj).map(get(key, 0)); + } + else if (obj instanceof NumberTextField) + { + return "" + ((NumberTextField)obj).getValue(); + } } + return "" + get(key, 0); } + + /** Print the model parameters to the given writer. */ + public void print(PrintWriter out) + { + String[] keys = getKeys(); + Arrays.sort(keys); + for(int i = 0; i < keys.length; i++) + { + if (isString(keys[i])) + out.println(keys[i] + ": " + getModelParameterText(keys[i])); + else if (isInteger(keys[i])) + { + String str = getModelParameterText(keys[i]); + String str2 = "" + get(keys[i], 0); + if (str.equals(FALSE_STRING)) + { + out.println(keys[i] + ": False"); + } + else if (str.equals(TRUE_STRING)) + { + out.println(keys[i] + ": True"); + } + else if (str.equals(str2)) + out.println(keys[i] + ": " + str); + else + out.println(keys[i] + ": " + str + " (" + str2 + ")"); + } + else + out.println(keys[i] + ": UNKNOWN OBJECT " + get(keys[i])); + } + } + + public int reviseMutatedValue(String key, int old, int current) { return current; } + public String reviseMutatedValue(String key, String old, String current) { return current; } } diff --git a/edisyn/MutationMap.java b/edisyn/MutationMap.java new file mode 100644 index 00000000..cc3c1e8f --- /dev/null +++ b/edisyn/MutationMap.java @@ -0,0 +1,85 @@ +/*** + Copyright 2017 by Sean Luke + Licensed under the Apache License version 2.0 +*/ + +package edisyn; + +import java.util.*; +import java.io.*; +import java.util.prefs.*; + +/** + @author Sean Luke +*/ + +public class MutationMap + { + // This is confusing, because although in the preferences + // the data is stored as TRUE = free, FALSE = not free + // In the map, the data is stored as STORED = not free + // Because FREE needs to be the default, common situation + HashSet map = new HashSet(); + + Preferences prefs; + + /** Returns whether the parameter is free to be mutated. */ + public boolean isFree(String key) + { + return !map.contains(key); + } + + public void setFree(String key, boolean free) + { + if (!free) map.add(key); + else map.remove(key); + + prefs.put(key, "" + free); + try + { + prefs.sync(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + public MutationMap(Preferences prefs) + { + // do a load + + this.prefs = prefs; + try + { + String[] keys = prefs.keys(); + for(int i = 0; i < keys.length; i++) + { + // each String holds a PARAMETER + // each Value holds a BOOLEAN + + if (prefs.get(keys[i], "true").equals("false")) + { + map.add(keys[i]); + } + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + public void clear() + { + try + { + prefs.clear(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + map = new HashSet(); + } + } diff --git a/edisyn/Synth.java b/edisyn/Synth.java index 01041c58..df9bbf34 100644 --- a/edisyn/Synth.java +++ b/edisyn/Synth.java @@ -11,6 +11,7 @@ import java.awt.geom.*; import javax.swing.border.*; import javax.swing.*; +import javax.swing.event.*; import java.awt.event.*; import java.util.*; import java.lang.reflect.*; @@ -34,971 +35,5655 @@ public static boolean recognize(byte[] data) public abstract class Synth extends JComponent implements Updatable { - public static int numOpenWindows = 0; - + // Counter for total number of open windows. When this drops to zero, + // the program will quit automatically. + static int numOpenWindows = 0; // The model proper - protected Model model; + public Model model; // Our own private random number generator - protected Random random; - // flag for whether we send midi when requested - boolean sendMIDI = false; + public Random random; // The file associated with the synth File file; // will the next load be a merge? If 0, we're not merging. Else it's the merge probability. double merging = 0.0; + + public JTabbedPane tabs = new JTabbedPane(); - public Midi.Tuple tuple; + public static final int MAX_FILE_LENGTH = 512 * 1024; // so we don't go on forever - - /** Handles mutation (or not) of keys declared immutable in the Model. - When the user is mutating (randomizing) a parameter, and it was declared - immutable, this method is called instead to let you change the parameter - as you like (or refuse to). */ - public abstract void immutableMutate(String key); - - /** Updates the model to reflect the following sysex patch dump for your synthesizer type. - If the patch is from current working memory and so doesn't need to be sent to the synth - to update it, return FALSE, else return TRUE. */ - public abstract boolean parse(byte[] data); - - /** Updates the model to reflect the following sysex or CC (or other!) message from your synthesizer. - You are free to IGNORE this message entirely. Patch dumps will generally not be sent this way; - and furthermore it is possible that this is crazy sysex from some other synth so you need to check for it. */ - public abstract void parseParameter(byte[] data); - - /** Merges the given model into yours, replacing elements of your model with the given - probability. */ - public abstract void merge(Model model, double probability); - - /** Produces a sysex patch dump suitable to send to a remote synthesizer. - If you return a zero-length byte array, nothing will be sent. - If tempModel is non-null, then it should be used to extract meta-parameters - such as the bank and patch number (stuff that's specified by gatherInfo(...). - Otherwise the primary model should be used. The primary model should be used - for all other parameters. toWorkingMemory indicates whether the patch should - be directed to working memory of the synth or to the patch number in tempModel. */ - public abstract byte[] emit(Model tempModel, boolean toWorkingMemory); - - /** Produces a sysex parameter change request for the given parameter. - If you return a zero-length byte array, nothing will be sent. */ - // TODO: maybe we should also permit CC and NRPN - public abstract byte[] emit(String key); - - /** Produces a sysex message to send to a synthesizer to request it to initiate - a patch dump to you. If you return a zero-length byte array, nothing will be sent. - If tempModel is non-null, then it should be used to extract meta-parameters - such as the bank and patch number or machine ID (stuff that's specified by gatherInfo(...). - Otherwise the primary model should be used. The primary model should be used - for all other parameters. - */ - public abstract byte[] requestDump(Model tempModel); - - /** Produces a sysex message to send to a synthesizer to request it to initiate - a patch dump to you for the CURRENT PATCH. If you return a zero-length byte array, - nothing will be sent. If tempModel is non-null, then it should be used to extract - meta-parameters such as the machine ID (stuff that's specified by gatherInfo(...). - Otherwise the primary model should be used. The primary model should be used - for all other parameters. - */ - public abstract byte[] requestCurrentDump(Model tempModel); - - /** Returns the expected length of a sysex patch dump for your type of synthesizer. */ - public abstract int getExpectedSysexLength(); + public static final int STATUS_SENDING_ALL_PARAMETERS = 0; + public static final int STATUS_UPDATING_ONE_PARAMETER = 1; - /** Gathers meta-parameters from the user via a JOptionPane, such as - patch number and bank number, which are used to specify where a patch - should be saved to or loaded from. These are typically also stored in - the primary model, but the user may want to change them so as to - write out to a different location for example. The model should not be - revised to hold the new values; but rather they should be placed into tempModel. - This method returns TRUE if the user provided the values, and FALSE - if he cancelled. - */ - public abstract boolean gatherInfo(String title, Model tempModel); - /** Create your own Synth-specific class version of this static method. - It will be called when the system wants to know if the given sysex patch dump - is for your kind of synthesizer. Return true if so, else false. */ - public static boolean recognize(byte[] data) - { - return false; - } + public JMenuBar menubar; + public JMenuItem transmitTo; + public JMenuItem transmitCurrent; + public JMenuItem writeTo; + public JMenuItem undoMenu; + public JMenuItem redoMenu; + public JMenuItem receiveCurrent; + public JMenuItem receivePatch; + public JCheckBoxMenuItem transmitParameters; + public JMenu merge; + public JMenuItem editMutationMenu; + public JCheckBoxMenuItem recombinationToggle; + public JMenuItem hillClimbMenu; + public JCheckBoxMenuItem testNotes; + public JComponent hillClimbPane; + public JMenuItem getAll; + public JMenuItem testIncomingController; + public JMenuItem testIncomingSynth; + public JCheckBoxMenuItem sendsAllSoundsOffBetweenNotesMenu; - /** Returns the current file associated with this editor, or null if there is none. */ - public File getFile() { return file; } + Model[] nudge = new Model[4]; + JMenuItem[] nudgeTowards = new JMenuItem[8]; + + // counter for note-on messages so we don't have a million note-off messages in a row + int noteOnTick; + + protected Undo undo = new Undo(this); + public Undo getUndo() { return undo; } - /** Sets whether sysex parameter changes should be sent in response to changes to the model. - You can set this to temporarily paralleize your editor when updating parameters. */ - public void setSendMIDI(boolean val) { sendMIDI = val; } + String copyPreamble; + public String getCopyPreamble() { return copyPreamble; } + public void setCopyPreamble(String preamble) { copyPreamble = preamble; } + + String copyType; + public String getCopyType() { return copyType; } + public void setCopyType(String type) { copyType = type; } + + ArrayList copyKeys; + public ArrayList getCopyKeys() { return copyKeys; } + public void setCopyKeys(ArrayList keys) { copyKeys = keys; } - /** Gets whether sysex parameter changes should be sent in response to changes to the model. */ - public boolean getSendMIDI() { return sendMIDI; } - /** Returns the model associated with this editor. */ public Model getModel() { return model; } - - /** Returns the name of the synthesizer */ - public abstract String getSynthName(); - /** Returns the name of the current patch. */ - public abstract String getPatchName(); - - /** Return true if the window can be closed and disposed of. You should do some cleanup - as necessary (the system will handle cleaning up the receiver and transmitters. */ - public abstract boolean requestCloseWindow(); - - - /** Updates the JFrame title to reflect the synthesizer type, the patch information, and the filename - if any. See Blofeld.updateTitle() for inspiration. */ - public void updateTitle() + /** Replaces the model with one whose hashmaps have been compacted. */ + public void compactModel() { - JFrame frame = ((JFrame)(SwingUtilities.getRoot(this))); - if (frame != null) - frame.setTitle(getSynthName().trim() + " " + - getPatchName().trim() + - (getFile() == null ? "" : " " + getFile().getName()) + - ((tuple == null || tuple.in == null) ? " (DISCONNECTED)" : "")); + model = ((Model)(model.clone())); } - + boolean testIncomingControllerMIDI; + boolean testIncomingSynthMIDI; - public Synth() + boolean parsingForMerge = false; + /** Indicates that we are a sacrificial synth which is parsing an incoming sysex dump and then will be merged with the main synth. */ + public boolean isParsingForMerge() { return parsingForMerge; } + + public JMenuItem copyTab = new JMenuItem("Copy Tab"); + public JMenuItem pasteTab = new JMenuItem("Paste Tab"); + public JMenuItem copyMutableTab = new JMenuItem("Copy Tab (Mutation Parameters Only)"); + public JMenuItem pasteMutableTab = new JMenuItem("Paste Tab (Mutation Parameters Only)"); + public JMenuItem resetTab = new JMenuItem("Reset Tab"); + + + //boolean useMapForRecombination = true; + boolean showingMutation = false; + /** Returns true if we're currently trying to merge with another patch. */ + public boolean isMerging() { return merging != 0.0; } + public boolean isShowingMutation() { return showingMutation; } + public void setShowingMutation(boolean val) + { + showingMutation = val; + if (val == true) + setLearningCC(false); + if (isShowingMutation()) + { + editMutationMenu.setText("Stop Editing Mutation Parameters"); + } + else + { + editMutationMenu.setText("Edit Mutation Parameters"); + } + updateTitle(); + repaint(); + } + public MutationMap mutationMap; + public String[] getMutationKeys() { - try { - System.setProperty("apple.laf.useScreenMenuBar", "true"); - System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Test"); - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + String[] keys = getModel().getKeys(); + ArrayList list = new ArrayList(); + for(int i = 0; i < keys.length; i++) + { + if (mutationMap.isFree(keys[i])) + list.add(keys[i]); } - catch(Exception e) { } + return (String[])(list.toArray(new String[0])); + } + + public Model buildModel() { return new Model(); } + + - model = new Model(); + /////// CREATION AND CONSTRUCTION + + public Synth() + { + model = buildModel(); model.register(Model.ALL_KEYS, this); + model.setUndoListener(undo); + ccmap = new CCMap(Prefs.getAppPreferences(getSynthNameLocal(), "CCKey"), + Prefs.getAppPreferences(getSynthNameLocal(), "CCType")); + mutationMap = new MutationMap(Prefs.getAppPreferences(getSynthNameLocal(), "Mutation")); + + undo.setWillPush(false); // instantiate undoes this random = new Random(System.currentTimeMillis()); + + perChannelCCs = ("" + getLastX("PerChannelCC", getSynthNameLocal(), false)).equalsIgnoreCase("true"); } - /** Updates the graphics rendering hints before drawing. */ - public static void prepareGraphics(Graphics g) - { - Graphics2D graphics = (Graphics2D) g; - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); - } + /** Builds a synth of the given CLASS, with the given synth NAME. + If THROWAWAY is true, then the window won't be sprouted and MIDI won't be set up. + If SETUPMIDI is false, then IDI won't be set up. The TUPLE provides the default + MIDI devices. */ - /** Returns whether the given sysex patch dump data is of the type for this particular synth. - This is done by ultimately calling the CLASS method public static boolean recognize(data) - that your synthesizer subclass is asked to implement. */ - public boolean recognizeLocal(byte[] data) - { - return recognize(Synth.this.getClass(), data); - } - - /** Returns whether the given sysex patch dump data is of the type for a synth of the given - class. This is done by ultimately calling the CLASS method - public static boolean recognize(data) that each synthesizer subclass is asked to implement. */ - public static boolean recognize(Class synthClass, byte[] data) + public static Synth instantiate(Class _class, String name, boolean throwaway, boolean setupMIDI, Midi.Tuple tuple) { try { - Method method = synthClass.getMethod("recognize", new Class[] { byte[].class }); - Object obj = method.invoke(null, data); - return ((Boolean)obj).booleanValue(); - } - catch (Exception e) - { - e.printStackTrace(); - return false; - } - } - - /** Returns TRUE with the given probability. */ - public boolean coinToss(double probability) - { - if (probability==0.0) return false; // fix half-open issues - else if (probability==1.0) return true; // fix half-open issues - else return random.nextDouble() < probability; - } - - /** Independently randomizes each parameter with the given probability. - If certain parameter values have been declared SPECIAL in the model, then if the - parameter is to be randomized, then with a 50% one of those values will - be chosen at random, otherwise one of *any* value will be chosen at random. - This allows you to bias certain critical parameter values (like "off" versus - 1...100). Additionally, if a parameter has been declared IMMUTABLE in the model, - OR if the parameter is a STRING, - then instead of randomizing the parameter, the method immutableMutate() is called - instead to allow you to do what you wish with it. */ - public void mutate(double probability) - { - String[] keys = model.getKeys(); - for(int i = 0; i < keys.length; i++) - { - if (coinToss(probability)) + Synth synth = (Synth)(_class.newInstance()); // this will setWillPush(false); + if (!throwaway) { - String key = keys[i]; - if (model.isImmutable(key)) - immutableMutate(key); - else if (model.minExists(key) && model.maxExists(key)) + synth.sprout(); + final JFrame frame = ((JFrame)(SwingUtilities.getRoot(synth))); + + if (Style.isMac()) { - int min = model.getMin(key); - int max = model.getMax(key); - int[] special = model.getSpecial(key); - if (special != null && coinToss(0.5)) + // When you pack a large frame in OS X, it appears to trigger a bug where if the + // Synth's frame is taller than the maximum window bounds, minus some slop, instead of + // setting it to that size, it'll minimize it to zero. :-( This workaround seems to do + // the job. The exact minimal slop value is 11: I'm setting it to 20 for good measure. + + Rectangle size = frame.getBounds(); + Rectangle bounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds(); + if (size.width > bounds.width) { - model.set(key, special[random.nextInt(special.length)]); + size.width = bounds.width; + frame.setBounds(size); } - else + if (size.height > bounds.height - 20) { - model.set(key, random.nextInt(max - min + 1) + min); + size.height = bounds.height - 20; + frame.setBounds(size); } } - else if (model.isString(key)) - { - // can't handle a string - immutableMutate(key); - } + + frame.show(); + if (setupMIDI) + synth.setupMIDI("Choose MIDI devices to send to and receive from.", tuple); + + // we call this here even though it's already been called as a result of frame.setVisible(true) + // because it's *after* setupMidi(...) and so it gives synths a chance to send + // a MIDI sysex message in response to the window becoming front. + synth.sendAllSoundsOff(); // not doSendAllSoundsOff(false), because we don't want to stop the test notes per se + synth.windowBecameFront(); } + synth.undo.setWillPush(true); + return synth; } - } - - /** Called by the model to update the synth whenever a parameter is changed. */ - public void update(String key, Model model) - { - if (getSendMIDI()) + catch (Exception e2) { - tryToSendSysex(emit(key)); + e2.printStackTrace(); +// disableMenuBar(); + JOptionPane.showMessageDialog(null, "An error occurred while creating the synth editor for \n" + name, "Creation Error", JOptionPane.ERROR_MESSAGE); +// enableMenuBar(); } - } + return null; + } + + + - /** Builds a receiver to attach to the current IN transmitter. The receiver - can handle merging and patch reception. */ - public Receiver buildInReceiver() + + + + // CC HANDLING AND MAPPING + + + // Our CC Map + CCMap ccmap; + + // Are we passing through CCs? + boolean passThroughCC; + boolean passThroughController; + Object passThroughCCLock = new Object[0]; + + // Are we doing per-channel CCs or per-Panel CCs? + boolean perChannelCCs = false; + + // Are we presently in learning mode? + boolean learning = false; + // If we're in learning mode, what learning type are we? One of CCMap.TYPE_ABSOLUTE_CC, + // or CCMap.TYPE_RELATIVE_CC_64, or CCMap.TYPE_RELATIVE_CC_0. + int learningType; + + // MenuItem for Absolute CC, so we can enable/diable it + JMenuItem learningMenuItem; + // MenuItem for RelativeCC0, so we can enable/diable it + //JMenuItem learningMenuItem0; + // MenuItem for RelativeCC64, so we can enable/diable it + JMenuItem learningMenuItem64; + // MenuItem for Passing through CCs, so we can check it + JCheckBoxMenuItem passThroughCCMenuItem; + // MenuItem for Passing through CCs, so we can check it + JCheckBoxMenuItem perChannelCCsMenuItem; + + // MenuItem for Passing through Controller Values, so we can check it + JCheckBoxMenuItem passThroughControllerMenuItem; + + /** Returns whether we are passing through CC */ + public boolean getPassThroughController() { synchronized(passThroughCCLock) { return passThroughController; } } + public void setPassThroughController(final boolean val) { - return new Receiver() - { - public void close() - { - } - - public void send(MidiMessage message, long timeStamp) + synchronized(passThroughCCLock) + { + passThroughController = val; + setLastX("" + val, "PassThroughController", getSynthNameLocal(), false); + SwingUtilities.invokeLater(new Runnable() { - final byte[] data = message.getMessage(); - - if (SwingUtilities.getRoot(Synth.this) == javax.swing.FocusManager.getCurrentManager().getActiveWindow() && - recognizeLocal(data)) + public void run() { - // I'm doing this in the Swing event thread because I figure it's multithreaded - SwingUtilities.invokeLater(new Runnable() - { - public void run() - { - if (merging != 0.0) - { - merging = 0.0; - setSendMIDI(false); - Synth newSynth = instantiate(Synth.this.getClass(), getSynthName(), true, false, tuple); - newSynth.parse(data); - merge(newSynth.getModel(), 0.5); - setSendMIDI(true); - sendAllParameters(); - updateTitle(); - } - else - { - sendMIDI = false; // so we don't send out parameter updates in response to reading/changing parameters - boolean sendParameters = parse(data); - sendMIDI = true; - if (sendParameters) - sendAllParameters(); - file = null; - updateTitle(); - } - } - }); + passThroughControllerMenuItem.setState(val); } - else // Maybe it's a local Parameter change in sysex or CC? - { - sendMIDI = false; // so we don't send out parameter updates in response to reading/changing parameters - parseParameter(data); - sendMIDI = true; - updateTitle(); - } - } - }; + }); + } } - - /** Builds a receiver to attach to the current KEY transmitter. The receiver - can resend all incoming requests to the OUT receiver. */ - public Receiver buildKeyReceiver() - { - return new Receiver() - { - public void close() - { - } - - public void send(MidiMessage message, long timeStamp) + + /** Returns whether we are passing through CC */ + public boolean getPassThroughCC() { synchronized(passThroughCCLock) { return passThroughCC; } } + + /** Sets whether we are pasing through CC */ + public void setPassThroughCC(final boolean val) + { + synchronized(passThroughCCLock) + { + passThroughCC = val; + setLastX("" + val, "PassThroughCC", getSynthNameLocal(), false); + SwingUtilities.invokeLater(new Runnable() { - if (SwingUtilities.getRoot(Synth.this) == javax.swing.FocusManager.getCurrentManager().getActiveWindow() && sendMIDI) + public void run() { - if (message instanceof ShortMessage) - { - ShortMessage newMessage = null; - - // stupidly, ShortMessage has no way of changing its channel, so we have to rebuild - ShortMessage s = (ShortMessage)message; - int status = s.getStatus(); - int channel = s.getChannel(); - int command = s.getCommand(); - int data1 = s.getData1(); - int data2 = s.getData2(); - boolean voiceMessage = ( status < 0xF0 ); - try - { - if (voiceMessage) - newMessage = new ShortMessage(status, channel, data1, data2); - else - newMessage = new ShortMessage(status, data1, data2); - - tryToSendMIDI(newMessage); - } - catch (InvalidMidiDataException e) - { - e.printStackTrace(); - } - } - else if (message instanceof SysexMessage) - { - tryToSendSysex(message.getMessage()); - } + passThroughCCMenuItem.setState(val); } - } - }; + }); + if (val == true && getLearningCC()) + setLearningCC(false); + } } - /** Same as setupMIDI(message, null); */ - public boolean setupMIDI(String message) + // Returns the name we should display for a given CC on the Title Bar + String nameForCC(int cc, int sub) { - return setupMIDI(message, null); + if (cc < 256) + { + int type = ccmap.getTypeForCCPane(cc, getCurrentTab()); + if (type == CCMap.TYPE_RELATIVE_CC_64) + //return "CC64(" + sub + ") " + cc; + return "RCC(" + sub + ") " + cc; + else if (type == CCMap.TYPE_RELATIVE_CC_0) + return "CC0(" + sub + ") " + cc; + else return "CC(" + sub + ") " + cc; + } + else return "NRPN(" + sub + ") " + (cc - 256); } - /** Lets the user set up the MIDI in/out/key devices. The old devices are provided in oldTuple, - or you may pass null in if there are no old devices. Returns TRUE if a new tuple was set up. */ - public boolean setupMIDI(String message, Midi.Tuple oldTuple) + /** Returns whether we're presently learning CCs */ + public boolean getLearningCC() { return learning; } + + /** Sets whether we're presently learning CCs. */ + public void setLearningCC(boolean val) { - Midi.Tuple result = Midi.getNewTuple(tuple, this, message, buildInReceiver(), buildKeyReceiver()); - boolean retval = false; - - if (result == Midi.FAILED) + learning = val; + model.clearLastKey(); + if (learning) { - JOptionPane.showOptionDialog(this, "An error occurred while trying to connect to the chosen MIDI devices.", - "Cannot Connect", JOptionPane.DEFAULT_OPTION, - JOptionPane.WARNING_MESSAGE, null, - new String[] { "Revert" }, "Revert"); - result = oldTuple; - } - else if (result == Midi.CANCELLED) - { - // nothing + setShowingMutation(false); + if (learningMenuItem != null) learningMenuItem.setText("Stop Mapping"); + //if (learningMenuItem0 != null) learningMenuItem0.setEnabled(false); + if (learningMenuItem64 != null) learningMenuItem64.setEnabled(false); } else { - retval = true; + if (learningMenuItem != null) learningMenuItem.setText("Map CC / NRPN"); + //if (learningMenuItem0 != null) learningMenuItem0.setEnabled(true); + if (learningMenuItem64 != null) learningMenuItem64.setEnabled(true); } - - if (tuple != null) - tuple.dispose(); - - tuple = result; - - setSendMIDI(true); updateTitle(); - return retval; } + + /** Clears all learned CCs, and turns off learning. */ + public void clearLearned() + { + ccmap.clear(); + learning = false; + setLearningCC(false); + } + + + - /** Removes the in/out/key devices. */ - public void disconnectMIDI() + + + + + + + + + + //////// SYNTHESIZER EDIT PANES + + /** All synthesizer editor pane classes in Edisyn */ + // https://www.logicbig.com/tutorials/core-java-tutorial/java-se-api/service-loader.html + public static final Class[] synths = ServiceLoader.load(Synth.class).stream().map(ServiceLoader.Provider::type).toArray(Class[]::new); + + /** All synthesizer names in Edisyn, one per class in synths */ + public static String[] getSynthNames() { - if (tuple != null) + String[] synthNames = new String[synths.length]; + for(int i = 0; i < synths.length; i++) { - if (tuple.in != null && tuple.inReceiver != null) - tuple.in.removeReceiver(tuple.inReceiver); - if (tuple.key != null && tuple.keyReceiver != null) - tuple.key.removeReceiver(tuple.keyReceiver); - tuple.dispose(); + try + { + synthNames[i] = "Synth with no getSynthName() method, oops"; + Method method = synths[i].getMethod("getSynthName", new Class[] { }); + synthNames[i] = (String)(method.invoke(null, new Object[] { } )); + } + catch (Exception e) + { + e.printStackTrace(); + } } - tuple = null; - setSendMIDI(true); - updateTitle(); + return synthNames; } - - - /** Attempts to send a NON-Sysex MIDI message. Returns false if (1) the data was empty or null (2) - synth has turned off the ability to send temporarily (3) the sysex message is not - valid (4) an error occurred when the receiver tried to send the data. */ - public boolean tryToSendMIDI(ShortMessage message) + + /** Returns the name of this synth, by calling getSynthName(). */ + public final String getSynthNameLocal() { - if (message == null) - return false; - - if (getSendMIDI()) + // This code is basically a copy of getSynthNames(). + // But we can't easily merge them, one has to be static and the other non-static + try + { + Method method = this.getClass().getMethod("getSynthName", new Class[] { }); + return (String)(method.invoke(null, new Object[] { } )); + } + catch (Exception e) + { + e.printStackTrace(); + } + return "Synth with no getSynthName() method, oops"; + } + + + + + + + + + + + + /////// UNDO + + /** Update the Undo and Redo menus to be enabled or disabled. */ + public void updateUndoMenus() + { + if (undo == null) // we could just be a scratch synth, not one with a window + return; + + if (redoMenu != null) + redoMenu.setEnabled( + undo.shouldShowRedoMenu()); + if (undoMenu != null) + undoMenu.setEnabled( + undo.shouldShowUndoMenu()); + } + + + + + + public static Window lastActiveWindow = null; + + /** Temporarily sets me to be the active synth. */ + protected void setActiveSynth(boolean val) { activeSynth = val; } + boolean activeSynth = false; + + public boolean amActiveSynth() + { + if (activeSynth) return true; + + Window activeWindow = javax.swing.FocusManager.getCurrentManager().getActiveWindow(); + Component synthWindow = SwingUtilities.getRoot(Synth.this); + + // we want to be either the currently active window, the parent of a dialog box which is the active window, or the last active window if the user is doing something else + return (synthWindow == activeWindow || (activeWindow != null && synthWindow == activeWindow.getOwner()) || + (activeWindow == null && lastActiveWindow == synthWindow)); + } + + + + + + + + + + + + ////// STUFF YOU MAY HAVE TO OVERRIDE + + + + + /// There are a lot of redundant methods here. You only have to override some of them. + + /// PARSING (LOADING OR RECEIVING) + /// When a message is received from the synthesizser, Edisyn will do this: + /// If the message is a Sysex Message, then + /// Call recognize(message data). If it returns true, then + /// Call parse(message data, fromFile) [we presume it's a dump or a load from a file] + /// Else + /// Call parseParameter(message data) [we presume it's a parameter change, or maybe something else] + /// Else if the message is a complete CC or NRPN message + /// Call handleSynthCCOrNRPN(message) [it's some CC or NRPN that your synth is sending us, maybe a parameter change?] + + /// SENDING A SINGLE PARAMETER OF KEY key + /// Call emitAll(key, status) + /// This calls emitAll(key) + /// This calls emit(key) + /// + /// You could override either of these methods, but probably not both. + + /// SENDING TO CURRENT + /// Call sendAllParameters(). This does: + /// If getSendsAllParametersInBulk(), this calls: + /// emitAll(tempModel, toWorkingMemory = true, toFile) + /// This calls emit(tempModel, toWorkingMemory = true, toFile) + /// Else for every key it calls: + /// Call emitAll(key) + /// This calls emit(key) + /// + /// You could override either of the emit...(tempModel...) methods, but probably not both. + /// You could override either of the emit...(key...) methods, but probably not both. + + /// SENDING TO A PATCH + /// Call gatherPatchInfo(...,tempModel,...) + /// If successful + /// Call changePatch(tempModel) + /// Call sendAllParameters(). This does: + /// If getSendsAllParametersInBulk(), this calls: + /// emitAll(tempModel, toWorkingMemory = true, toFile) + /// This calls emit(tempModel, toWorkingMemory = true, toFile) + /// Else for every key it calls: + /// Call emitAll(key) + /// This calls emit(key) + /// + /// You could override either of the emit...(tempModel...) methods, but probably not both. + /// You could override either of the emit...(key...) methods, but probably not both. + + /// WRITING OR SAVING + /// Call gatherPatchInfo(...,tempModel,...) + /// If successful + /// Call emitAll(tempModel, toWorkingMemory = false, toFile) + /// This calls emit(tempModel, toWorkingMemory = false, toFile) + /// Call changePatch(tempModel) + /// + /// You could override either of the emit methods, but probably not both. + /// Note that saving strips out the non-sysex bytes from emitAll. + + /// SAVING + /// Call emitAll(tempModel, toWorkingMemory, toFile) + /// This calls emit(tempModel, toWorkingMemory, toFile) + /// + /// You could override either of the emit methods, but probably not both. + /// Note that saving strips out the non-sysex bytes from emitAll. + + /// REQUESTING A PATCH + /// If we're requesting the CURRENT patch + /// Call performRequestCurrentDump() + /// this then calls requestCurrentDump() + /// Else + /// Call gatherPatchInfo(...,tempModel,...) + /// If successful + /// Call performRequestDump(tempModel) + /// This calls changePatch(tempModel) + /// Then it calls requestDump(tempModel) + /// + /// You could override performRequestCurrentDump or requestCurrentDump, but probably not both. + /// Similarly, you could override performRequestDump or requestDump, but probably not both + + /// ADDITIONAL COMMONLY OVERRIDEN METHODS + /// + /// getSynthName() // you must override this + /// getPatchName(getModel()) // you ought to override this, it returns null by default + /// getSendsAllParametersInBulk() // override this to return FALSE if parameters must be sent one at a time rather than emitted as sysex + /// getDefaultResourceFileName() // return the filename of the default init file + /// getHTMLResourceFileName() // return the filename of the default HTML file + /// requestCloseWindow() // override this to query the user before the window is closed + /// revisePatchName(name) // tweak patch name to be valid. This is a utility method that you commonly override AND call + /// reviseID(id) // tweak the id to be valid + /// revise() // tweak all the keys to be within valid ranges. There's a default form which you might wish to override. + /// getPauseBetweenMIDISends() // return the pause (in ms) between MIDI messages if the synth needs them slower + /// getPauseAfterChangePatch() // return the pause after a PC + /// sprout() // typically you override this (calling super of course) to disable certain menus + /// windowBecameFront() // override this to be informed that your patch window became the front window (you might switch something on the synth) + + /** Changes the patch and bank to reflect the information in tempModel. + You may need to call simplePause() if your synth requires a pause after a patch change. */ + public void changePatch(Model tempModel) { } + + + public static int PARSE_FAILED = 0; + public static int PARSE_INCOMPLETE = 1; + public static int PARSE_SUCCEEDED = 2; + public static int PARSE_SUCCEEDED_UNTITLED = 3; + public static int PARSE_CANCELLED = 4; + + /** Updates the model to reflect the following sysex patch dump for your synthesizer type. + FROMFILE indicates that the parse is from a sysex file. + There are several possible things you can return: + - PARSE_CANCELLED indicates that the user cancelled the parse and the editor data was not changed. + - PARSE_FAILED indicates that the parse was not successful and the editor data was not changed. + - PARSE_INCOMPLETE indicates that the parse was not fully performed -- for example, + the Yamaha TX81Z needs two separate dumps before it has a full patch, so we return + PARSE_INCOMPLETE on the first one, and PARSE_SUCCEEDED only on the last one). + - PARSE_SUCCEEDED indicates that the parse was completed and the patch is fully modified. + - PARSE SUCCEEDED_UNTITLED indicates the same, except that assuming the patch was read + from a file, an alternative version of the patch has been substituted, and so the patch + filename should be untitled. For example, the DX7 can alternatively load bank-sysex + patches and extract a patch from the bank; in this case the patch filename should not + be the bank sysex filename. */ + public int parse(byte[] data, boolean fromFile) { return PARSE_FAILED; } + + /** Updates the model to reflect the following sysex message from your synthesizer. + You are free to IGNORE this message entirely. Patch dumps will generally not be sent this way; + and furthermore it is possible that this is crazy sysex from some other synth so you need to check for it. */ + public void parseParameter(byte[] data) { return; } + + /** Produces a sysex patch dump suitable to send to a remote synthesizer as one + OR MORE sysex dumps or other MIDI messages. Each sysex dump is a separate byte array, + and other midi messages are MIDI message objects. + If you return a zero-length array, nothing will be sent. + If tempModel is non-null, then it should be used to extract meta-parameters + such as the bank and patch number (stuff that's specified by gatherPatchInfo(...). + Otherwise the primary model should be used. The primary model should be used + for all other parameters. toWorkingMemory indicates whether the patch should + be directed to working memory of the synth or to the patch number in tempModel. + +

If TOFILE is true, then we are emitting to a file, not to the synthesizer proper. + +

It is assumed that the NON byte-array elements may be stripped out if this + emit is done to a file. + +

The default version of this method simply calls emit() and returns its + value as the first subarray. If you have a synthesizer (like the TX81Z) which + dumps a single patch as multiple sysex dumps, override this to send the patch + properly. + */ + public Object[] emitAll(Model tempModel, boolean toWorkingMemory, boolean toFile) + { + byte[] result = emit(tempModel, toWorkingMemory, toFile); + if (result == null || + result.length == 0) + return new Object[0]; + else + return new Object[] { result }; + } + + /** Produces a sysex patch dump suitable to send to a remote synthesizer. + If you return a zero-length byte array, nothing will be sent. + If tempModel is non-null, then it should be used to extract meta-parameters + such as the bank and patch number (stuff that's specified by gatherPatchInfo(...). + Otherwise the primary model should be used. The primary model should be used + for all other parameters. toWorkingMemory indicates whether the patch should + be directed to working memory of the synth or to the patch number in tempModel. + +

If TOFILE is true, then we are emitting to a file, not to the synthesizer proper. + +

Note that this method will only be called by emitAll(...). So if you + have overridden emitAll(...) you don't need to implement this method. */ + public byte[] emit(Model tempModel, boolean toWorkingMemory, boolean toFile) { return new byte[0]; } + + /** Produces one or more sysex parameter change requests for the given parameter as one + OR MORE sysex dumps or other MIDI messages. Each sysex dump is a separate byte array, + and other midi messages are MIDI message objects. The status integer indicates under + what condition emitAll(...) is being called, such as STATUS_SENDING_ALL_PARAMETERS + or STATUS_UPDATING_ONE_PARAMETER. By default, this calls emitAll(key); + + If you return a zero-length byte array, nothing will be sent. */ + public Object[] emitAll(String key, int status) + { + return emitAll(key); + } + + /** Produces one or more sysex parameter change requests for the given parameter as one + OR MORE sysex dumps or other MIDI messages. Each sysex dump is a separate byte array, + and other midi messages are MIDI message objects. + + If you return a zero-length byte array, nothing will be sent. */ + public Object[] emitAll(String key) + { + byte[] result = emit(key); + if (result == null || + result.length == 0) + return new Object[0]; + else + return new Object[] { result }; + } + + /** Produces a sysex parameter change request for the given parameter. + If you return a zero-length byte array, nothing will be sent. + +

Note that this method will only be called by emitAll(...). So if you + have overridden emitAll(...) you don't need to implement this method. */ + public byte[] emit(String key) { return new byte[0]; } + + /** Produces a sysex message to send to a synthesizer to request it to initiate + a patch dump to you. If you return a zero-length byte array, nothing will be sent. + If tempModel is non-null, then it should be used to extract meta-parameters + such as the bank and patch number or machine ID (stuff that's specified by gatherPatchInfo(...). + Otherwise the primary model should be used. The primary model should be used + for all other parameters. + */ + public byte[] requestDump(Model tempModel) { return new byte[0]; } + + /** Produces a sysex message to send to a synthesizer to request it to initiate + a patch dump to you for the CURRENT PATCH. If you return a zero-length byte array, + nothing will be sent. + */ + public byte[] requestCurrentDump() { return new byte[0]; } + + /** Performs a request for a dump of the patch indicated in tempModel. + This method by default does a changePatch() as necessary, then calls + requestDump(...) and submits it to tryToSendSysex(...), + but you can override it to do something more sophisticated. + Note that if your synthesizer can load patches without switching to them, you + should only change patches if changePatch is true. An example of when + changePatch will be false: when doing a merge (you'd like to merge an external + patch into this one but stay where you are). Another example is when a multi-patch + wants to pop up a single patch to display it. */ + public void performRequestDump(Model tempModel, boolean changePatch) + { + if (changePatch) + performChangePatch(tempModel); + + tryToSendSysex(requestDump(tempModel)); + } + + /** Performs a request for a dump of the patch indicated in the current model. + This method by default calls requestCurrentDump(...) and submits it to tryToSendSysex(...), + but you can override it to do something more sophisticated. */ + public void performRequestCurrentDump() + { + tryToSendSysex(requestCurrentDump()); + } + + /** Gathers meta-parameters from the user via a JOptionPane, such as + patch number and bank number, which are used to specify where a patch + should be saved to or loaded from. These are typically also stored in + the primary model, but the user may want to change them so as to + write out to a different location for example. The model should not be + revised to hold the new values; but rather they should be placed into tempModel. + This method returns TRUE if the user provided the values, and FALSE + if he cancelled. + +

If writing is TRUE, then the purpose of this info-gathering is to find + a place to write or send a patch TO. Otherwise its purpose is to read a patch FROM + somewhere. Some synths allow patches to be read from many locations but written only + to specific ones (because the others are read-only). + */ + public boolean gatherPatchInfo(String title, Model tempModel, boolean writing) { return false; } + + /** Create your own Synth-specific class version of this static method. + It will be called when the system wants to know if the given sysex patch dump + is for your kind of synthesizer. Return true if so, else false. */ + private static boolean recognize(byte[] data) + { + // The Synth.java version of this method is obviously never called. + // But your subclass's version will be called. + return false; + } + + /** Create your own Synth-specific class version of this static method. + It will be called when the system wants to know if the given sysex patch dump + is some kind of bulk (multi-patch dump) for your kind of synthesizer. Return true if so, else false. + If you don't implement this method, by default it returns false -- this would be the + case where you don't support any bulk loading via parse(...). */ + private static boolean recognizeBulk(byte[] data) + { + // The Synth.java version of this method is obviously never called. + // But your subclass's version will be called. + return false; + } + + /** For the given patch, return the number of sysex dumps that must be + provided to handle patches of this kind. Typically this is 1, but + for some synths, such as the TX81Z, the answer is 2. */ + private static int getNumSysexDumpsPerPatch(byte[] data) + { + return 1; + } + + /** Override this to handle CC or NRPN messages which arrive from the synthesizer. */ + public void handleSynthCCOrNRPN(Midi.CCData data) + { + // do nothing by default + } + + /** Returns the name of the synthesizer. You should make your own + static version of this method in your synth panel subclass. */ + private static String getSynthName() { return "Override Me"; } + + /** Returns a Model with the next patch location (bank, number, etc.) beyond the one provided in the given model. + If the model provided contains the very last patch location, you should wrap around. */ + public Model getNextPatchLocation(Model model) + { + return null; + } + + /** Returns the patch location as a simple and short string, such as "B100" for "Bank B Number 100". + The default implementation returns null; if this method returns null, + then bulk downloading will not be available. */ + public String getPatchLocationName(Model model) + { + return null; + } + + /** Returns the name of the patch in the given model, or null if there is no such thing. */ + public String getPatchName(Model model) { return null; } + + /** Return true if the window can be closed and disposed of. You should do some cleanup + as necessary (the system will handle cleaning up the receiver and transmitters. + By default this just returns true. */ + public boolean requestCloseWindow() { return true; } + + /** Function for tweaking a name to make it valid for display in the editor. + The default version just does a right-trim of whitespace on the name. You + may wish to override this to also restrict the valid characters and the name + length. */ + public String revisePatchName(String name) + { + // right-trim the name + int i = name.length()-1; + while (i >= 0 && Character.isWhitespace(name.charAt(i))) i--; + name = name.substring(0, i+1); + return name; + } + + /** If the provided id is correct, return it. If the provided id is *null* or incorrect, + provide the id from getModel(). Return null if there is no such thing as an id for this machine. */ + public String reviseID(String id) + { + return null; + } + + public void revise() + { + revise(model); + } + + boolean printRevised = true; + + /** Only revises / issues warnings on out-of-bounds numerical parameters. + You probably want to override this to check more stuff. */ + public void revise(Model model) + { + String[] keys = model.getKeys(); + for(int i = 0; i < keys.length; i++) + { + String key = keys[i]; + if (!model.isString(key) && + model.minExists(key) && + model.maxExists(key)) + { + // verify + int val = model.get(key); + if (val < model.getMin(key)) + { model.set(key, model.getMin(key)); if (printRevised) System.err.println("Warning (Synth): Revised " + key + " from " + val + " to " + model.get(key));} + if (val > model.getMax(key)) + { model.set(key, model.getMax(key)); if (printRevised) System.err.println("Warning (Synth): Revised " + key + " from " + val + " to " + model.get(key));} + } + } + + } + + /** Override this to make sure that at *least* the given time (in Milliseconds) has transpired between MIDI sends. */ + public double getPauseBetweenMIDISends() { return 0; } + long getNanoPauseBetweenMIDISends() { return (long)(getPauseBetweenMIDISends() * 1000000.0); } + + /** Override this to make sure that the given additional time (in ms) has transpired between MIDI patch changes. */ + public int getPauseAfterChangePatch() { return 0; } + + /** Override this to make sure that the given additional time (in ms) has transpired between sending all parameters and anything else (such as playing a note) */ + public int getPauseAfterSendAllParameters() { return 0; } + + /** Override this to make sure that the given additional time (in ms) has transpired between sending each parameter via emitAll(key). */ + public int getPauseAfterSendOneParameter() { return 0; } + + /** Override this to return TRUE if, after a patch write, we need to change to the patch *again* so as to load it into memory. */ + public boolean getShouldChangePatchAfterWrite() { return false; } + + /** Override this to return TRUE if, after recieving a NON-MERGE patch from the synthesizer, we should turn around and sendAllParameters() to it. + This commonly is needed in some synth multi-mode editors, where program changes have no effect (they don't switch to a new multi-mode synth), + and so we'll receive the correct patch but the synthesizer won't switch to it. We can turn around and emit changes to it to get the right + sound in the edit buffer. */ + public boolean getSendsParametersAfterNonMergeParse() { return false; } + + /** Return the filename of your default sysex file (for example "MySynth.init"). Should be located right next to the synth's class file ("MySynth.class") */ + public String getDefaultResourceFileName() { return null; } + + /** Return the filename of your HTML About file (for example "MySynth.html"). Should be located right next to the synth's class file ("MySynth.class") */ + public String getHTMLResourceFileName() { return null; } + + /** Override this as you see fit to do something special when your window becomes front. */ + public void windowBecameFront() { } + + /** Returns whether the synth sends its patch dump (TRUE) as one single sysex dump or by + sending multiple separate parameter change requests (FALSE). By default this is TRUE. */ + public boolean getSendsAllParametersInBulk() { return true; } + + /** Returns whether the synth sends raw CC or cooked CC (such as for NRPN) to update parameters. The default is FALSE (cooked or nothing). */ + public boolean getExpectsRawCCFromSynth() { return false; } + + /** Returns whether the synth should send parameters immediately after a successful load or load-merge from disk. */ + public boolean getSendsParametersAfterLoad() { return true; } + + + + + + + // UTILITY METHODS FOR BUILDING MIDI MESSAGES + + /** Builds a sequence of CCs for an NRPN message. */ + public Object[] buildNRPN(int channel, int parameter, int value) + { + try + { + int p_msb = (parameter >>> 7); + int p_lsb = (parameter & 127); + int v_msb = (value >>> 7); + int v_lsb = (value & 127); + return new Object[] + { + new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, 99, (byte)p_msb), + new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, 98, (byte)p_lsb), + new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, 6, (byte)v_msb), + new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, 38, (byte)v_lsb), + + // can't have these right now, it freaks out the PreenFM2 + //new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, 101, (byte)127), + //new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, 100, (byte)127), + }; + } + catch (InvalidMidiDataException e) + { + e.printStackTrace(); + return new Object[0]; + } + } + + /** Builds a short (7-bit) CC. */ + public Object[] buildCC(int channel, int parameter, int value) + { + try + { + return new Object[] + { + new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, parameter, (byte)(value & 127)) + }; + } + catch (InvalidMidiDataException e) + { + e.printStackTrace(); + return new Object[0]; + } + } + + + /** Builds a sequence of CCs for a 14-bit CC message. The parameter must be 0...31. */ + public Object[] buildLongCC(int channel, int parameter, int value) + { + try + { + int v_msb = (value >>> 7); + int v_lsb = (value & 127); + return new Object[] + { + new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, parameter, (byte)v_msb), + new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, parameter + 32, (byte)v_lsb) + }; + } + catch (InvalidMidiDataException e) + { + e.printStackTrace(); + return new Object[0]; + } + } + + + /** Builds a Program Change message. */ + public Object[] buildPC(int channel, int program) + { + try + { + return new Object[] + { + new ShortMessage(ShortMessage.PROGRAM_CHANGE, channel, program, 0) + }; + } + catch (InvalidMidiDataException e) + { + e.printStackTrace(); + return new Object[0]; + } + } + + + + + // MIDI INTERFACE + + /** The Synth's MIDI Tuple */ + public Midi.Tuple tuple; + + // The synth's MIDI interface + Midi midi = new Midi(); + + // flag for whether sending MIDI is temporarily turned off or not + boolean sendMIDI = true; // we can send MIDI + + /** Returns the current merge probability. If the value is 0.0, + then merging is not occurring. */ + public double getMergeProbability() + { + return merging; + } + + /** Returns the current merge probability. If the value is 0.0, + then merging is not occurring. */ + public void setMergeProbability(double val) + { + if (val < 0) val = 0; + if (val > 1) val = 1; + merging = val; + } + + + /** Returns whether the mutation map should be used for recombination. */ + /* + public boolean getUsesMapForRecombination() + { + return useMapForRecombination; + } + */ + + + /** Builds a receiver to attach to the current IN transmitter. The receiver + can handle merging and patch reception. */ + public Receiver buildInReceiver() + { + return new Receiver() + { + public void close() + { + } + + public void send(final MidiMessage message, long timeStamp) + { + // I'm doing this in the Swing event thread because I figure it's multithreaded + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + if (amActiveSynth()) + { + if (message instanceof SysexMessage) + { + final byte[] data = message.getMessage(); + + if (recognizeLocal(data)) + { + if (merging != 0.0) + { + if (merge(data, merging)) + { + merging = 0.0; + } + } + else + { + // we turn off MIDI because parse() calls revise() which triggers setParameter() with its changes + setSendMIDI(false); + undo.setWillPush(false); + Model backup = (Model)(model.clone()); + int result = parse(data, false); + incomingPatch = (result == PARSE_SUCCEEDED || result == PARSE_SUCCEEDED_UNTITLED); + if (result == PARSE_CANCELLED) + { + // nothing + } + else if (result == PARSE_FAILED) + { + showSimpleError("Merge Error", "Could not merge the patch."); + } + + undo.setWillPush(true); + if (!backup.keyEquals(getModel())) // it's changed, do an undo push + undo.push(backup); + setSendMIDI(true); + if (getSendsParametersAfterNonMergeParse()) + sendAllParameters(); + file = null; + } + + // this last statement fixes a mystery. When I call Randomize or Reset on + // a Blofeld or on a Microwave, all of the widgets update simultaneously. + // But on a Blofeld Multi or Microwave Multi they update one at a time. + // I've tried a zillion things, even moving all the widgets from the Blofeld Multi + // into the Blofeld, and it makes no difference! For some reason the OS X + // repaint manager is refusing to coallesce their repaint requests. So I do it here. + repaint(); + + updateTitle(); + } + else // Maybe it's a local Parameter change in sysex? + { + // we don't do undo here. It's not great but PreenFM2 etc. would wreak havoc + boolean willPush = undo.getWillPush(); + undo.setWillPush(false); + + sendMIDI = false; // so we don't send out parameter updates in response to reading/changing parameters + parseParameter(data); + sendMIDI = true; + updateTitle(); + + undo.setWillPush(willPush); + } + } + else if (message instanceof ShortMessage) + { + ShortMessage sm = (ShortMessage)message; + if (sm.getCommand() == ShortMessage.CONTROL_CHANGE) + { + boolean willPush = undo.getWillPush(); + undo.setWillPush(false); + + // we don't do undo here. It's not great but PreenFM2 etc. would wreak havoc + sendMIDI = false; // so we don't send out parameter updates in response to reading/changing parameters + // let's try parsing it + handleInRawCC(sm); + if (!getReceivesPatchesInBulk()) + { + incomingPatch = true; + } + sendMIDI = true; + updateTitle(); + + undo.setWillPush(willPush); + } + } + } + if (testIncomingSynthMIDI) + { + showSimpleMessage("Incoming MIDI from Synthesizer", "A MIDI message has arrived from the Synthesizer:\n" + Midi.format(message) + "\nTime: " + timeStamp); + testIncomingSynthMIDI = false; + testIncomingSynth.setText("Report Next Synth MIDI"); + } + } + }); + + } + }; + } + + /** Builds a receiver to attach to the current KEY transmitter. The receiver + can resend all incoming requests to the OUT receiver. */ + public Receiver buildKeyReceiver() + { + return new Receiver() + { + public void close() + { + } + + public void send(final MidiMessage message, long timeStamp) + { + // I'm doing this in the Swing event thread because I figure it's multithreaded + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + if (amActiveSynth()) + { + if (message instanceof ShortMessage) + { + ShortMessage shortMessage = (ShortMessage)message; + try + { + // we intercept a message if: + // 1. It's a CC (maybe NRPN) + // 2. We're not passing through CC + // 3. It's the right channel OR our key channel is OMNI OR we're doing per-channel CCs + if (tuple != null && + !getPassThroughCC() && + shortMessage.getCommand() == ShortMessage.CONTROL_CHANGE && + (shortMessage.getChannel() == tuple.keyChannel || tuple.keyChannel == tuple.KEYCHANNEL_OMNI || perChannelCCs)) + { + // we intercept this + handleKeyRawCC(shortMessage); + messageFromController(message, true, false); + } + + // We send the message to the synth if: + // 1. We didn't intercept it + // 2. We pass through data to the synth + else if (tuple != null && getPassThroughController()) + { + // pass it on! + ShortMessage newMessage = null; + + // In order to pass on, we have to make a new one. But + // stupidly, ShortMessage has no way of changing its channel, so we have to rebuild + ShortMessage s = (ShortMessage)message; + int status = s.getStatus(); + int channel = s.getChannel(); + int data1 = s.getData1(); + int data2 = s.getData2(); + boolean voiceMessage = ( status < 0xF0 ); + + // should we attempt to reroute to the synth? + if (channel == tuple.keyChannel || tuple.keyChannel == tuple.KEYCHANNEL_OMNI) + { + channel = getVoiceMessageRoutedChannel(channel, getChannelOut()); + } + + if (voiceMessage) + newMessage = new ShortMessage(status, channel, data1, data2); + else + newMessage = new ShortMessage(status, data1, data2); + + tryToSendMIDI(newMessage); + messageFromController(newMessage, false, true); + } + else + { + messageFromController(message, false, false); + } + } + catch (InvalidMidiDataException e) + { + e.printStackTrace(); + messageFromController(message, false, false); + } + } + else if (message instanceof SysexMessage && passThroughController) + { + tryToSendSysex(message.getMessage()); + messageFromController(message, false, true); + } + } + if (testIncomingControllerMIDI) + { + showSimpleMessage("Incoming MIDI from Controller", "A MIDI message has arrived from the Controller:\n" + Midi.format(message) + "\nTime: " + timeStamp); + testIncomingControllerMIDI = false; + testIncomingController.setText("Report Next Controller MIDI"); + } + } + }); + } + }; + } + + public void messageFromController(MidiMessage message, boolean interceptedForInternalUse, boolean routedToSynth) { return; } + + public int getVoiceMessageRoutedChannel(int incomingChannel, int synthChannel) { return synthChannel; } + + /** Sets whether sysex parameter changes should be sent in response to changes to the model. + You can set this to temporarily paralleize your editor when updating parameters. */ + public void setSendMIDI(boolean val) { sendMIDI = val; } + + /** Gets whether sysex parameter changes should be sent in response to changes to the model. */ + public boolean getSendMIDI() { return sendMIDI; } + + /** Same as setupMIDI(message, null), with a default "you are disconnected" message. */ + public boolean setupMIDI() { return setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.", null); } + + /** Same as setupMIDI(message, oldTuple), with a default "you are disconnected" message. */ + public boolean setupMIDI(Midi.Tuple oldTuple) { return setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.", oldTuple); } + + /** Lets the user set up the MIDI in/out/key devices. TheMIDI old devices are provided in oldTuple, + or you may pass null in if there are no old devices. Returns TRUE if a new tuple was set up. */ + public boolean setupMIDI(String message, Midi.Tuple oldTuple) + { + Midi.Tuple result = Midi.getNewTuple(oldTuple, this, message, buildInReceiver(), buildKeyReceiver()); + boolean retval = false; + + if (result == Midi.FAILED) + { + disableMenuBar(); + JOptionPane.showOptionDialog(this, "An error occurred while trying to connect to the chosen MIDI devices.", + "Cannot Connect", JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, null, + new String[] { "Revert" }, "Revert"); + enableMenuBar(); + } + else if (result == Midi.CANCELLED) + { + // nothing + } + else + { + if (tuple != null) + tuple.dispose(); + tuple = result; // update + setSendMIDI(true); + updateTitle(); + retval = true; + } + + return retval; + } + + public void resetColors() + { + setLastColor("background-color", Style.DEFAULT_BACKGROUND_COLOR); + setLastColor("text-color", Style.DEFAULT_TEXT_COLOR); + setLastColor("a-color", Style.DEFAULT_COLOR_A); + setLastColor("b-color", Style.DEFAULT_COLOR_B); + setLastColor("c-color", Style.DEFAULT_COLOR_C); + setLastColor("dynamic-color", Style.DEFAULT_DYNAMIC_COLOR); + setLastColor("unset-color", Style.DEFAULT_UNSET_COLOR); + Style.updateColors(); + } + + public void setupColors() + { + Color backgroundColor = getLastColor("background-color", Style.DEFAULT_BACKGROUND_COLOR); + Color textColor = getLastColor("text-color", Style.DEFAULT_TEXT_COLOR); + Color aColor = getLastColor("a-color", Style.DEFAULT_COLOR_A); + Color bColor = getLastColor("b-color", Style.DEFAULT_COLOR_B); + Color cColor = getLastColor("c-color", Style.DEFAULT_COLOR_C); + Color dynamicColor = getLastColor("dynamic-color", Style.DEFAULT_DYNAMIC_COLOR); + Color dialColor = getLastColor("unset-color", Style.DEFAULT_UNSET_COLOR); + + ColorWell background = new ColorWell(backgroundColor); + ColorWell text = new ColorWell(textColor); + ColorWell a = new ColorWell(aColor); + ColorWell b = new ColorWell(bColor); + ColorWell c = new ColorWell(cColor); + ColorWell dynamic = new ColorWell(dynamicColor); + ColorWell dial = new ColorWell(dialColor); + + disableMenuBar(); + boolean result = Synth.showMultiOption(this, + new String[] { "Background ", "Text ", "Color A ", "Color B ", "Color C ", "Highlights ", "Dials " }, + new JComponent[] { background, text, a, b, c, dynamic, dial }, + "Update Colors", + "Note: after changing colors, currently
open windows may look scrambled,
but new windows will look correct.
"); + enableMenuBar(); + + if (result) + { + setLastColor("background-color", background.getColor()); + setLastColor("text-color", text.getColor()); + setLastColor("a-color", a.getColor()); + setLastColor("b-color", b.getColor()); + setLastColor("c-color", c.getColor()); + setLastColor("dynamic-color", dynamic.getColor()); + setLastColor("unset-color", dial.getColor()); + Style.updateColors(); + } + + } + + + public void performChangePatch(Model tempModel) + { + changePatch(tempModel); + int p = getPauseAfterChangePatch(); + if (p > 0) + simplePause(p); + } + + /** Does a basic sleep for the given ms. You should only call this if you can't + achieve the same thing by overriding one of the getPause methods, such as + getPauseAfterChangePatch()... */ + public void simplePause(int ms) + { + if (ms == 0) return; + try { long l = System.currentTimeMillis(); Thread.currentThread().sleep(ms);} + catch (Exception e) { e.printStackTrace(); } + } + + + long lastMIDISend = 0; + // this is different from the simple pause in that it only pauses + // if that much time hasn't already transpired between midi sends + void midiPause(long expectedPause) + { + if (expectedPause <= 0) return; + + long pauseSoFar = System.nanoTime() - lastMIDISend; + if (pauseSoFar >= 0 && pauseSoFar < expectedPause) + { + long pause = expectedPause - pauseSoFar; + // verify that pause is rational + if (pause < 0L) pause = 0L; + if (pause > 100000000L) pause = 100000000L; // 10ms, still within the int range and not so slow as to make the UI impossible + try { Thread.currentThread().sleep((int)(pause / 1000000), (int)(pause % 1000000)); } + catch (Exception e) { e.printStackTrace(); } + } + } + + + Object[] midiSendLock = new Object[0]; + + /** Attempts to send a NON-Sysex MIDI message. Returns false if (1) the data was empty or null (2) + synth has turned off the ability to send temporarily (3) the sysex message is not + valid (4) an error occurred when the receiver tried to send the data. */ + public boolean tryToSendMIDI(MidiMessage message) + { + if (message == null) + { return false; } + else if (!amActiveSynth()) + { return false; } + else if (getSendMIDI()) + { + if (tuple == null) return false; + + Receiver receiver = tuple.out; + if (receiver == null) return false; + + // compute pause + try { if (!noMIDIPause) midiPause(getNanoPauseBetweenMIDISends()); } + catch (Exception e) + { + e.printStackTrace(); + } + + synchronized(midiSendLock) + { + try + { + receiver.send(message, -1); + } + catch (IllegalStateException e) + { + // This happens when the device has closed itself and we're still trying to send to it. + // For example if the user rips the USB cord for his device out of the laptop. In this + // case we'll also disconnect + SwingUtilities.invokeLater(new Runnable() + { + public void run() { doDisconnectMIDI(); } + }); + return false; + } + } + lastMIDISend = System.nanoTime(); + return true; + } + else + return false; + } + + /** If you are sending a sysex message as fragments with pauses in-between them, + what is the length of the pause? By default this is 0 (no pause). */ + public int getPauseBetweenSysexFragments() { return 0; } + + /** Indicates that sysex messages are not sent as fragments. */ + public static int NO_SYSEX_FRAGMENT_SIZE = 0; + + /** If you are sending a sysex message as fragments with pauses in-between them, + how large are the fragments? By default, this is NO_SYSEX_FRAGMENT_SIZE. */ + public int getSysexFragmentSize() { return NO_SYSEX_FRAGMENT_SIZE; } + + /** Attempts to send a single MIDI sysex message. Returns false if (1) the data was empty or null (2) + synth has turned off the ability to send temporarily (3) the sysex message is not + valid (4) an error occurred when the receiver tried to send the data. */ + public boolean tryToSendSysex(byte[] data) + { + if (data == null || data.length == 0) + return false; + else if (!amActiveSynth()) + { + return false; + } + + for(int i = 1; i < data.length - 1; i++) + { + if (data[i] < 0) // uh oh, high byte + { + String s = ""; + for(int j = 0; j <= i; j++) + s = s + (data[j] < 0 ? data[j] + 255 : data[j]) + " "; + new RuntimeException("High byte in sysex found. First example is byte #" + i + "\n" + s).printStackTrace(); + break; + } + } + + if (getSendMIDI()) + { + if (tuple == null) return false; + Receiver receiver = tuple.out; + if (receiver == null) return false; + + // compute pause + midiPause(getNanoPauseBetweenMIDISends()); + + try { + SysexMessage message = new SysexMessage(data, data.length); + synchronized(midiSendLock) + { + int fragmentSize = getSysexFragmentSize(); + if (fragmentSize <= NO_SYSEX_FRAGMENT_SIZE || message.getLength() <= fragmentSize) + { + receiver.send(message, -1); + } + else + { + MidiMessage[] messages = Midi.DividedSysex.divide(message, 16); + for(int i = 0; i < messages.length; i++) + { + if (i > 0) simplePause(getPauseBetweenSysexFragments()); + receiver.send(messages[i], -1); + } + } + } + lastMIDISend = System.nanoTime(); + return true; + } + catch (InvalidMidiDataException e) { e.printStackTrace(); return false; } + catch (IllegalStateException e2) + { + // This happens when the device has closed itself and we're still trying to send to it. + // For example if the user rips the USB cord for his device out of the laptop. + SwingUtilities.invokeLater(new Runnable() + { + public void run() { doDisconnectMIDI(); } + }); + return false; + } + } + else + return false; + } + + /** If you get a index=0, outOf=0, we're done */ + public void sentMIDI(Object datum, int index, int outOf) { } + + /** Attempts to send several MIDI sysex or other kinds of messages. Data elements can be + one of four things: (1) null, which is essentially a no-op (2) a byte[], which indicates + a sysex message (3) a fully constructed and populated MidiMessage (possibly including + sysex messages), and (4) an Integer, which will be used to indicate a pause for that + many milliseconds before sending the next message. + +

Returns false if + (1) the data was empty or null + (2) the synth has turned off the ability to send temporarily + (3) a message was not valid + (4) an error occurred when the receiver tried to send the data. + */ + public boolean tryToSendMIDI(Object[] data) + { + if (data == null) return false; + if (data.length == 0) return false; + for(int i = 0; i < data.length; i++) + { + if (data[i] == null) + { + continue; + } + else if (data[i] instanceof Integer) + { + simplePause(((Integer)data[i]).intValue()); + continue; + } + else if (data[i] instanceof byte[]) + { + byte[] sysex = (byte[])(data[i]); + if (!tryToSendSysex(sysex)) + { sentMIDI(null, 0, 0); return false; } + } + else if (data[i] instanceof MidiMessage) + { + MidiMessage message = (MidiMessage)(data[i]); + if (!tryToSendMIDI(message)) + { sentMIDI(null, 0, 0); return false; } + } + sentMIDI(data[i], i, data.length); + } + sentMIDI(null, 0, 0); + return true; + } + + /** Returns whether the given sysex patch dump data is a bulk (multi-patch) dump of the type for a synth of the given + class. This is done by ultimately calling the CLASS method + public static boolean recognize(data) that each synthesizer subclass is asked to implement. */ + public static boolean recognizeBulk(Class synthClass, byte[] data) + { + try + { + Method method = synthClass.getMethod("recognizeBulk", new Class[] { byte[].class }); + Object obj = method.invoke(null, data); + return ((Boolean)obj).booleanValue(); + } + catch (Exception e) + { + return false; + } + } + + /** Returns the number of sysex dumps typically required to load a synth + patch of the given class. For example, the TX81Z requires *two* sysex dumps + to load properly, while nearly all other synthesizers require 1. */ + public static int getNumSysexDumpsPerPatch(Class synthClass, byte[] data) + { + try + { + Method method = synthClass.getMethod("getNumSysexDumpsPerPatch", new Class[] { byte[].class }); + Object obj = method.invoke(null, data); + return ((Integer)obj).intValue(); + } + catch (Exception e) + { + return 1; + } + } + + /** Returns whether the given sysex patch dump data is of the type for a synth of the given + class. This is done by ultimately calling the CLASS method + public static boolean recognize(data) that each synthesizer subclass is asked to implement. */ + public static boolean recognize(Class synthClass, byte[] data) + { + try + { + Method method = synthClass.getMethod("recognize", new Class[] { byte[].class }); + Object obj = method.invoke(null, data); + return ((Boolean)obj).booleanValue(); + } + catch (Exception e) + { + e.printStackTrace(); + return false; + } + } + + /** Returns whether the given sysex patch dump data is of the type for this particular synth. + This is done by ultimately calling the CLASS method public static boolean recognize(data) + that your synthesizer subclass is asked to implement. */ + public final boolean recognizeLocal(byte[] data) + { + return recognize(Synth.this.getClass(), data); + } + + /** Returns whether the given sysex patch dump data is a bulk (multi-patch) dump of the type for this particular synth. + This is done by ultimately calling the CLASS method public static boolean recognizeBulk(data) + that your synthesizer subclass is asked to implement. */ + public final boolean recognizeBulkLocal(byte[] data) + { + return recognizeBulk(Synth.this.getClass(), data); + } + + /** Returns the number of sysex dumps typically required to load a synth + patch of this kind. For example, the TX81Z requires *two* sysex dumps + to load properly, while nearly all other synthesizers require 1. */ + public final int getNumSysexDumpsPerPatchLocal(byte[] data) + { + return getNumSysexDumpsPerPatch(Synth.this.getClass(), data); + } + + void handleInRawCC(ShortMessage message) + { + if (getExpectsRawCCFromSynth()) + { + handleSynthCCOrNRPN(midi.synthParser.handleRawCC(message.getChannel(), message.getData1(), message.getData2())); + } + else + { + Midi.CCData ccdata = midi.synthParser.processCC(message, false, false); + if (ccdata != null) + { + handleSynthCCOrNRPN(ccdata); + } + } + } + + void handleKeyRawCC(ShortMessage message) + { + Midi.CCData ccdata = midi.controlParser.processCC(message, false, false); + if (ccdata != null) + { + if (ccdata.type == Midi.CCDATA_TYPE_NRPN) + { + ccdata.number += CCMap.NRPN_OFFSET; + } + if (learning) + { + String key = model.getLastKey(); + if (key != null) + { + int sub = getCurrentTab(); + if (perChannelCCs) + sub = ccdata.channel; + ccmap.setKeyForCCPane(ccdata.number, sub, key); + if (ccdata.type == Midi.CCDATA_TYPE_NRPN) + ccmap.setTypeForCCPane(ccdata.number, sub, CCMap.TYPE_NRPN); // though it doesn't really matter + else + ccmap.setTypeForCCPane(ccdata.number, sub, learningType); + setLearningCC(false); + } + } + else + { + int sub = getCurrentTab(); + if (perChannelCCs) + sub = ccdata.channel; + String key = ccmap.getKeyForCCPane(ccdata.number, sub); + if (key != null) + { + // handle increment/decrement + if (ccdata.increment) + { + ccdata.value = ccdata.value + model.get(key); + } + + // handle the situation where the range is larger than the CC/NRPN message, + // else bump it to min + if (model.minExists(key) && model.maxExists(key)) + { + if (ccdata.type == Midi.CCDATA_TYPE_RAW_CC) + { + int type = ccmap.getTypeForCCPane(ccdata.number, sub); + int min = model.getMin(key); + int max = model.getMax(key); + int val = model.get(key); + + if (type == CCMap.TYPE_ABSOLUTE_CC) + { + if (max - min + 1 > 127) // uh oh + { + ccdata.value = (int)(((max - min + 1) / (double) 127) * ccdata.value); + } + else + { + ccdata.value = min + ccdata.value; + } + } + else if (type == CCMap.TYPE_RELATIVE_CC_64) + { + ccdata.value = val + ccdata.value - 64; + } + else if (type == CCMap.TYPE_RELATIVE_CC_0) + { + if (ccdata.value < 64) + ccdata.value = val + ccdata.value; + else + ccdata.value = val + ccdata.value - 128; + } + else + { + throw new RuntimeException("This Shouldn't Happen"); + } + } + else if (ccdata.type == Midi.CCDATA_TYPE_NRPN) + { + int min = model.getMin(key); + int max = model.getMax(key); + if (max - min + 1 > 16383) // uh oh, but very unlikely + { + ccdata.value = (int)(((max - min + 1) / (double) 16383) * ccdata.value); + } + else + { + ccdata.value = min + ccdata.value; + } + } + } + + model.setBounded(key, ccdata.value); + } + } + } + } + + /** Merges in a dumped patch with the existing one and returns TRUE. + In some rare cases, such as for the TX81Z, merging requires multiple + sysex dumps to come back. In this case, if not all the dumps have + arrived (a merge call will be made for each one), return FALSE, until + you finally have collected enough data to do a merge, at which point + you should return super.merge(revisedData, probability). */ + public boolean merge(byte[] data, double probability) + { + setSendMIDI(false); + undo.setWillPush(false); + Model backup = (Model)(model.clone()); + + Synth newSynth = instantiate(Synth.this.getClass(), getSynthNameLocal(), true, false, tuple); + newSynth.setSendMIDI(false); + newSynth.parsingForMerge = true; + int result = newSynth.parse(data, false); + newSynth.parsingForMerge = false; + newSynth.setSendMIDI(true); + if (result == PARSE_CANCELLED) + { + // nothing + return false; + } + else if (result == PARSE_FAILED) + { + showSimpleError("Merge Error", "Could not merge the patch."); + return false; + } + else + { + model.recombine(random, newSynth.getModel(), getMutationKeys(), probability); //useMapForRecombination ? getMutationKeys() : model.getKeys() + revise(); // just in case + + undo.setWillPush(true); + if (!backup.keyEquals(getModel())) // it's changed, do an undo push + undo.push(backup); + setSendMIDI(true); + sendAllParameters(); + + // this last statement fixes a mystery. When I call Randomize or Reset on + // a Blofeld or on a Microwave, all of the widgets update simultaneously. + // But on a Blofeld Multi or Microwave Multi they update one at a time. + // I've tried a zillion things, even moving all the widgets from the Blofeld Multi + // into the Blofeld, and it makes no difference! For some reason the OS X + // repaint manager is refusing to coallesce their repaint requests. So I do it here. + repaint(); + return true; + } + } + + /** Returns the current channel (0--15, NOT 1--16) with which we are using to + communicate with the synth. If there is no MIDI tuple, this returns 0. */ + public int getChannelOut() + { + int channel = 0; + if (tuple != null) + channel = tuple.outChannel - 1; + return channel; + } + + /** Sends all the parameters in a patch to the synth. + +

If sendsAllParametersInBulk was set to TRUE, then this is done by sending + a single patch write to working memory, which may not be supported by all synths. + + Otherwise this is done by sending each parameter separately, which isn't as fast. + The default sends each parameter separately. + */ + public void sendAllParameters() + { + if (!getSendMIDI()) + return; // don't bother! MIDI is off + + if (getSendsAllParametersInBulk()) + { + boolean sent = tryToSendMIDI(emitAll(getModel(), true, false)); + if (sent) + simplePause(getPauseAfterSendAllParameters()); + } + else + { + boolean sent = false; + String[] keys = getModel().getKeys(); + for(int i = 0; i < keys.length; i++) + { + if (sent = tryToSendMIDI(emitAll(keys[i], STATUS_SENDING_ALL_PARAMETERS)) || sent) + simplePause(getPauseAfterSendOneParameter()); + } + if (sent) + simplePause(getPauseAfterSendAllParameters()); + } + } + + + + + + + + + + + + + + ////////// GUI UTILITIES + + + public int getNumTabs() + { + return tabs.getTabCount(); + } + + public int getSelectedTabIndex() + { + return tabs.getSelectedIndex(); + } + + /** Returns -1 if there is no such tab. */ + public int getIndexOfTabTitle(String title) + { + for(int i = 0; i < getNumTabs(); i++) + { + if (tabs.getTitleAt(i).equals(title)) + return i; + } + return -1; + } + + /** Returns FALSE if the tab could not be selected */ + public boolean setSelectedTabIndex(int index) + { + try + { + tabs.setSelectedIndex(index); + return true; + } + catch (Exception e) + { + return false; + } + } + + public JComponent getSelectedTab() + { + return (JComponent)(tabs.getSelectedComponent()); + } + + public String getSelectedTabTitle() + { + return tabs.getTitleAt(tabs.getSelectedIndex()); + } + + public JComponent insertTab(String title, JComponent component, int index) + { + JScrollPane pane = new JScrollPane(component, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + pane.setViewportBorder(null); + pane.setBorder(null); + tabs.insertTab(title, null, pane, null, index); + return pane; + } + + public JComponent addTab(String title, JComponent component) + { + JScrollPane pane = new JScrollPane(component, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + pane.setViewportBorder(null); + pane.setBorder(null); + tabs.addTab(title, pane); + return pane; + } + + public void removeTab(String title) + { + int idx = tabs.indexOfTab(title); + if (idx != -1) + tabs.remove(idx); + } + + + boolean inSimpleError; + + /** Display a simple error message. */ + public void showSimpleError(String title, String message) + { + // A Bug in OS X (perhaps others?) Java causes multiple copies of the same Menu event to be issued + // if we're popping up a dialog box in response, and if the Menu event is caused by command-key which includes + // a modifier such as shift. To get around it, we're just blocking multiple recursive message dialogs here. + + if (inSimpleError) return; + inSimpleError = true; + disableMenuBar(); + JOptionPane.showMessageDialog(this, message, title, JOptionPane.ERROR_MESSAGE); + enableMenuBar(); + inSimpleError = false; + } + + /** Display a simple error message. */ + public void showSimpleMessage(String title, String message) + { + // A Bug in OS X (perhaps others?) Java causes multiple copies of the same Menu event to be issued + // if we're popping up a dialog box in response, and if the Menu event is caused by command-key which includes + // a modifier such as shift. To get around it, we're just blocking multiple recursive message dialogs here. + + if (inSimpleError) return; + inSimpleError = true; + disableMenuBar(); + JOptionPane.showMessageDialog(this, message, title, JOptionPane.INFORMATION_MESSAGE); + enableMenuBar(); + inSimpleError = false; + } + + /** Display a simple (OK / Cancel) confirmation message. Return the result (ok = true, cancel = false). */ + public boolean showSimpleConfirm(String title, String message) + { + disableMenuBar(); + boolean ret = (JOptionPane.showConfirmDialog(Synth.this, message, title, + JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null) == JOptionPane.OK_OPTION); + enableMenuBar(); + return ret; + } + + + + /** Perform a JOptionPane confirm dialog with MUTLIPLE widgets that the user can select. The widgets are provided + in the array WIDGETS, and each has an accompanying label in LABELS. Returns TRUE if the user performed + the operation, FALSE if cancelled. */ + public static boolean showMultiOption(Synth synth, String[] labels, JComponent[] widgets, String title, String message) + { + WidgetList list = new WidgetList(labels, widgets); + + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout()); + JPanel p = new JPanel(); + p.setLayout(new BorderLayout()); + p.add(new JLabel(" "), BorderLayout.NORTH); + p.add(new JLabel(message), BorderLayout.CENTER); + p.add(new JLabel(" "), BorderLayout.SOUTH); + panel.add(list, BorderLayout.CENTER); + panel.add(p, BorderLayout.NORTH); + + synth.disableMenuBar(); + boolean ret = (JOptionPane.showConfirmDialog(synth, panel, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null) == JOptionPane.OK_OPTION); + synth.enableMenuBar(); + return ret; + } + + + /** Returns the synth name to be used in the title bar. */ + public String getTitleBarSynthName() { return getSynthNameLocal(); } + + /** Updates the JFrame title to reflect the synthesizer type, the patch information, and the filename if any. */ + public void updateTitle() + { + JFrame frame = ((JFrame)(SwingUtilities.getRoot(this))); + if (frame != null) + { + String synthName = getTitleBarSynthName().trim(); + String fileName = (file == null ? " Untitled" : " " + file.getName()); + String disconnectedWarning = ((tuple == null || tuple.in == null) ? " DISCONNECTED" : ""); + String downloadingWarning = (patchTimer != null ? " DOWNLOADING" : ""); + String learningWarning = (learning ? " LEARNING" + + (model.getLastKey() != null ? " " + model.getLastKey() + + (model.getRange(model.getLastKey()) > 0 ? "[" + model.getRange(model.getLastKey()) + "]" : "") + + (ccmap.getCCForKey(model.getLastKey()) >= 0 ? "=" + nameForCC(ccmap.getCCForKey(model.getLastKey()), + ccmap.getPaneForKey(model.getLastKey())) : "") + : "") : ""); + String restrictingWarning = (isShowingMutation() ? " MUTATION PARAMETERS" : ""); + + frame.setTitle(synthName + fileName + " " + disconnectedWarning + downloadingWarning + learningWarning + restrictingWarning); + } + } + + public int getCurrentTab() + { + return tabs.getSelectedIndex(); + } + + public void setCurrentTab(int tab) + { + // int len = tabs.getTabCount(); + if (tab >= tabs.getTabCount()) + return; + if (tab < 0) + return; + tabs.setSelectedIndex(tab); + } + + + + public int readFully(byte[] array, InputStream input) + { + int current = 0; + try + { + while(true) + { + int total = input.read(array, current, array.length - current); + if (total <= 0) break; + current += total; + } + } + catch (IOException ex) { ex.printStackTrace(); } + return current; + } + + + + + ////////// DEFAULTS + + + + // Note that this isn't wrapped in undo, so we can block it at instantiation + public void loadDefaults() + { + String defaultResourceFileName = getDefaultResourceFileName(); + if (defaultResourceFileName == null) return; + + InputStream stream = getClass().getResourceAsStream(getDefaultResourceFileName()); + if (stream != null) + { + try + { + byte[] buffer = new byte[MAX_FILE_LENGTH]; // better not be longer than this + int size = readFully(buffer, stream); + + // now shorten + byte[] data = new byte[size]; + System.arraycopy(buffer, 0, data, 0, size); + + // parse + setSendMIDI(false); + parse(data, true); + setSendMIDI(true); + model.setUndoListener(undo); // okay, redundant, but that way the pattern stays the same + + // this last statement fixes a mystery. When I call Randomize or Reset on + // a Blofeld or on a Microwave, all of the widgets update simultaneously. + // But on a Blofeld Multi or Microwave Multi they update one at a time. + // I've tried a zillion things, even moving all the widgets from the Blofeld Multi + // into the Blofeld, and it makes no difference! For some reason the OS X + // repaint manager is refusing to coallesce their repaint requests. So I do it here. + repaint(); + } + catch (Exception e) + { + e.printStackTrace(); + } + finally + { + try { stream.close(); } + catch (IOException e) { } + } + } + else + { + System.err.println("Warning (Synth): Didn't Parse"); + } + } + + + /** Given a preferences path X for a given synth, sets X to have the given value.. + Also sets the global path X to the value. Typically this method is called by a + a cover function (see for example setLastSynth(...) ) */ + static final void setLastX(String value, String x, String synthName) + { + setLastX(value, x, synthName, false); + } + + /** Given a preferences path X for a given synth, sets X to have the given value.. + Also sets the global path X to the value. Typically this method is called by a + a cover function (see for example setLastSynth(...) ) */ + public static void setLastX(String value, String x, String synthName, boolean onlySetInSynth) + { + if (synthName != null) + { + java.util.prefs.Preferences app_p = Prefs.getAppPreferences(synthName, "Edisyn"); + app_p.put(x, value); + Prefs.save(app_p); + } + if (!onlySetInSynth) + { + setLastX(value, x); + } + } + + /** Given a preferences path X for a given synth, sets X to have the given value.. + Also sets the global path X to the value. Typically this method is called by a + a cover function (see for example setLastSynth(...) ) */ + static final void setLastX(String value, String x) + { + java.util.prefs.Preferences global_p = Prefs.getGlobalPreferences("Data"); + global_p.put(x, value); + Prefs.save(global_p); + } + + /** Given a preferences path X for a given synth, returns the value stored in X. + If there is no such value, then returns the value stored in X in the globals. + If there again is no such value, returns null. Typically this method is called by a + a cover function (see for example getLastSynth(...) ) */ + public static final String getLastX(String x, String synthName) + { + return getLastX(x, synthName, false); + } + + /** Given a preferences path X for a given synth, returns the value stored in X. + If there is no such value, then returns the value stored in X in the globals. + If there again is no such value, returns null. Typically this method is called by a + a cover function (see for example getLastSynth(...) ) */ + public static String getLastX(String x, String synthName, boolean onlyGetFromSynth) + { + String lastDir = null; + if (synthName != null) + { + lastDir = Prefs.getAppPreferences(synthName, "Edisyn").get(x, null); + } + + if (!onlyGetFromSynth && lastDir == null) + { + lastDir = getLastX(x); + } + + return lastDir; + } + + private static final String getLastX(String x) + { + return Prefs.getGlobalPreferences("Data").get(x, null); + } + + + // sets the last directory used by load, save, or save as + public void setLastDirectory(String path) { setLastX(path, "LastDirectory", getSynthNameLocal(), false); } + // sets the last directory used by load, save, or save as + public String getLastDirectory() { return getLastX("LastDirectory", getSynthNameLocal(), false); } + + // sets the last synthesizer opened via the global window. + static void setLastSynth(String synth) { setLastX(synth, "Synth", null, false); } + // gets the last synthesizer opened via the global window. + static String getLastSynth() { return getLastX("Synth", null, false); } + + public static Color getLastColor(String key, Color defaultColor) + { + String val = getLastX(key); + if (val == null) { return defaultColor; } + Scanner scan = new Scanner(val); + if (!scan.hasNextInt()) { return defaultColor; } + int red = scan.nextInt(); + if (!scan.hasNextInt()) { return defaultColor; } + int green = scan.nextInt(); + if (!scan.hasNextInt()) { return defaultColor; } + int blue = scan.nextInt(); + if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) { return defaultColor; } + return new Color(red, green, blue); + } + + static void setLastColor(String key, Color color) + { + if (color == null) return; + String val = "" + color.getRed() + " " + color.getGreen() + " " + color.getBlue(); + setLastX(val, key); + } + + + + + + + + + + + + + + + + /////////// SPROUT AND MENU HANDLING + + + public SynthPanel findPanel() + { + Component tab = tabs.getSelectedComponent(); + if (tab instanceof JScrollPane) + { + Component inner = ((JScrollPane)tab).getViewport().getView(); + if (inner instanceof SynthPanel) + { + return (SynthPanel) inner; + } + } + return null; + } + + public void tabChanged() + { + // cancel learning + setLearningCC(false); + + Component tab = tabs.getSelectedComponent(); + if (tab == hillClimbPane) + { + hillClimb.startup(); + } + else + { + hillClimb.shutdown(); + } + + pasteTab.setEnabled(false); + pasteMutableTab.setEnabled(false); + copyTab.setEnabled(false); + copyMutableTab.setEnabled(false); + resetTab.setEnabled(false); + + SynthPanel panel = findPanel(); + if (panel != null) + { + if (panel.isPasteable()) + { + copyTab.setEnabled(true); + copyMutableTab.setEnabled(true); + } + if (!panel.isUnresettable()) + { + resetTab.setEnabled(true); + } + if (panel.isPasteCompatible(getCopyPreamble())) + { + pasteTab.setEnabled(true); + pasteMutableTab.setEnabled(true); + } + } + } + + + public JFrame sprout() + { + setLayout(new BorderLayout()); + add(tabs, BorderLayout.CENTER); + tabs.addChangeListener(new ChangeListener() + { + public void stateChanged(ChangeEvent e) + { + tabChanged(); + } + }); + hillClimb = new HillClimb(this); + + String html = getHTMLResourceFileName(); + if (html != null) + tabs.addTab("About", new HTMLBrowser(this.getClass().getResourceAsStream(html))); + + final JFrame frame = new JFrame(); + menubar = new JMenuBar(); + frame.setJMenuBar(menubar); + JMenu menu = new JMenu("File"); + menubar.add(menu); + + JMenuItem _new = new JMenuItem("New " + getSynthNameLocal()); + _new.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(_new); + _new.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + doNew(); + } + }); + + JMenu newSynth = new JMenu("New Synth"); + menu.add(newSynth); + String[] synthNames = getSynthNames(); + for(int i = 0; i < synths.length; i++) + { + final int _i = i; + JMenuItem synthMenu = new JMenuItem(synthNames[i]); + synthMenu.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + doNewSynth(_i); + } + }); + newSynth.add(synthMenu); + } + + JMenuItem _copy = new JMenuItem("Duplicate Synth"); + _copy.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(_copy); + _copy.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + doDuplicateSynth(); + } + }); + + JMenuItem open = new JMenuItem("Load..."); + open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(open); + open.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setActiveSynth(true); + if (doOpen(false) && getSendsParametersAfterLoad()) + sendAllParameters(); + setActiveSynth(false); + } + }); + + JMenuItem openAndMerge = new JMenuItem("Load and Merge..."); + menu.add(openAndMerge); + openAndMerge.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + Synth.this.merging = 1.0; + setActiveSynth(true); + if (doOpen(true) && getSendsParametersAfterLoad()) + sendAllParameters(); + setActiveSynth(false); + Synth.this.merging = 0.0; + } + }); + menu.addSeparator(); + + JMenuItem close = new JMenuItem("Close Window"); + close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(close); + close.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doCloseWindow(); + } + }); + + menu.addSeparator(); + + JMenuItem save = new JMenuItem("Save"); + save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(save); + save.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSave(); + } + }); + + JMenuItem saveAs = new JMenuItem("Save As..."); + menu.add(saveAs); + saveAs.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSaveAs(); + } + }); + + menu.addSeparator(); + + getAll = new JMenuItem("Batch Download..."); + menu.add(getAll); + getAll.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doGetAllPatches(); + } + }); + + if (getPatchLocationName(getModel()) == null) + { + // not implemented. :-( + getAll.setEnabled(false); + } + + JMenuItem saveToText = new JMenuItem("Export to Text..."); + menu.add(saveToText); + saveToText.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSaveText(); + } + }); + + menu = new JMenu("Edit"); + menubar.add(menu); + + undoMenu = new JMenuItem("Undo"); + undoMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(undoMenu); + undoMenu.setEnabled(false); + undoMenu.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doUndo(); + } + }); + + redoMenu = new JMenuItem("Redo"); + redoMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + menu.add(redoMenu); + redoMenu.setEnabled(false); + redoMenu.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doRedo(); + } + }); + + + menu.addSeparator(); + + JMenuItem reset = new JMenuItem("Reset"); + menu.add(reset); + reset.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doReset(); + } + }); + + JMenu randomize = new JMenu("Randomize"); + menu.add(randomize); + JMenuItem randomize1 = new JMenuItem("Randomize by 1%"); + randomize.add(randomize1); + JMenuItem randomize2 = new JMenuItem("Randomize by 2%"); + randomize.add(randomize2); + JMenuItem randomize5 = new JMenuItem("Randomize by 5%"); + randomize.add(randomize5); + JMenuItem randomize10 = new JMenuItem("Randomize by 10%"); + randomize.add(randomize10); + JMenuItem randomize25 = new JMenuItem("Randomize by 25%"); + randomize.add(randomize25); + JMenuItem randomize50 = new JMenuItem("Randomize by 50%"); + randomize.add(randomize50); + JMenuItem randomize100 = new JMenuItem("Randomize by 100%"); + randomize.add(randomize100); + + randomize.addSeparator(); + + JMenuItem undoAndRandomize = new JMenuItem("Undo and Randomize Again"); + randomize.add(undoAndRandomize); + + randomize1.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMutate(0.01); + } + }); +// randomize1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + + randomize2.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMutate(0.02); + } + }); +// randomize2.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + + randomize5.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMutate(0.05); + } + }); +// randomize5.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + + randomize10.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMutate(0.1); + } + }); +// randomize10.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_U, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + + randomize25.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMutate(0.25); + } + }); + // randomize25.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + + randomize50.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMutate(0.5); + } + }); +// randomize50.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + + randomize100.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMutate(1.0); + } + }); +// randomize100.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + + undoAndRandomize.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + if (undo.shouldShowUndoMenu()) + { + if (lastMutate > 0.0) + { + doUndo(); + doMutate(lastMutate); + } + else + { + showSimpleError("Undo", "Can't Undo and Randomize Again: no previous randomize!"); + } + } + else + { + showSimpleError("Undo", "Can't Undo and Randomize Again: no previous randomize!"); + } + } + }); + undoAndRandomize.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + + + JMenu nudgeMenu = new JMenu("Nudge"); + menu.add(nudgeMenu); + + nudgeTowards[0] = new JMenuItem("Towards 1"); + nudgeMenu.add(nudgeTowards[0]); + nudgeTowards[0].addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNudge(0); + } + }); + nudgeTowards[0].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_1, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.ALT_MASK)); + + nudgeTowards[1] = new JMenuItem("Towards 2"); + nudgeMenu.add(nudgeTowards[1]); + nudgeTowards[1].addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNudge(1); + } + }); + nudgeTowards[1].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_2, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.ALT_MASK)); + + nudgeTowards[2] = new JMenuItem("Towards 3"); + nudgeMenu.add(nudgeTowards[2]); + nudgeTowards[2].addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNudge(2); + } + }); + nudgeTowards[2].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_3, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.ALT_MASK)); + + nudgeTowards[3] = new JMenuItem("Towards 4"); + nudgeMenu.add(nudgeTowards[3]); + nudgeTowards[3].addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNudge(3); + } + }); + nudgeTowards[3].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_4, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.ALT_MASK)); + + nudgeMenu.addSeparator(); + + nudgeTowards[4] = new JMenuItem("Away from 1"); + nudgeMenu.add(nudgeTowards[4]); + nudgeTowards[4].addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNudge(4); + } + }); + nudgeTowards[4].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_5, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK | InputEvent.ALT_MASK)); + + nudgeTowards[5] = new JMenuItem("Away from 2"); + nudgeMenu.add(nudgeTowards[5]); + nudgeTowards[5].addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNudge(5); + } + }); + nudgeTowards[5].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_6, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK | InputEvent.ALT_MASK)); + + nudgeTowards[6] = new JMenuItem("Away from 3"); + nudgeMenu.add(nudgeTowards[6]); + nudgeTowards[6].addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNudge(6); + } + }); + nudgeTowards[6].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_7, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK | InputEvent.ALT_MASK)); + + nudgeTowards[7] = new JMenuItem("Away from 4"); + nudgeMenu.add(nudgeTowards[7]); + nudgeTowards[7].addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNudge(7); + } + }); + nudgeTowards[7].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_8, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK | InputEvent.ALT_MASK)); + + nudgeMenu.addSeparator(); + + JMenuItem undoAndNudge = new JMenuItem("Undo and Nudge Again"); + nudgeMenu.add(undoAndNudge); + undoAndNudge.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + if (undo.shouldShowUndoMenu()) + { + if (lastNudge > -1) + { + doUndo(); + doNudge(lastNudge); + } + else + { + showSimpleError("Undo", "Can't Undo and Nudge Again: no previous nudge!"); + } + } + else + { + showSimpleError("Undo", "Can't Undo and Nudge Again: no previous nudge!"); + } + } + }); + undoAndNudge.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + + nudgeMenu.addSeparator(); + + JMenuItem nudgeSet1 = new JMenuItem("Set 1"); + nudgeMenu.add(nudgeSet1); + nudgeSet1.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSetNudge(0); + } + }); + + JMenuItem nudgeSet2 = new JMenuItem("Set 2"); + nudgeMenu.add(nudgeSet2); + nudgeSet2.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSetNudge(1); + } + }); + + JMenuItem nudgeSet3 = new JMenuItem("Set 3"); + nudgeMenu.add(nudgeSet3); + nudgeSet3.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSetNudge(2); + } + }); + + JMenuItem nudgeSet4 = new JMenuItem("Set 4"); + nudgeMenu.add(nudgeSet4); + nudgeSet4.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSetNudge(3); + } + }); + + // reset the nudges now + for(int i = 0; i < nudge.length; i++) + doSetNudge(i); + + + + nudgeMenu.addSeparator(); + + ButtonGroup nudgeMutationButtonGroup = new ButtonGroup(); + JMenu nudgeMutation = new JMenu("Set Nudge Mutation"); + nudgeMenu.add(nudgeMutation); + JRadioButtonMenuItem nudgeMutation0 = new JRadioButtonMenuItem("0%"); + nudgeMutation.add(nudgeMutation0); + nudgeMutationButtonGroup.add(nudgeMutation0); + JRadioButtonMenuItem nudgeMutation1 = new JRadioButtonMenuItem("1%"); + nudgeMutation.add(nudgeMutation1); + nudgeMutationButtonGroup.add(nudgeMutation1); + JRadioButtonMenuItem nudgeMutation2 = new JRadioButtonMenuItem("2%"); + nudgeMutation.add(nudgeMutation2); + nudgeMutationButtonGroup.add(nudgeMutation2); + JRadioButtonMenuItem nudgeMutation5 = new JRadioButtonMenuItem("5%"); + nudgeMutation.add(nudgeMutation5); + nudgeMutationButtonGroup.add(nudgeMutation5); + JRadioButtonMenuItem nudgeMutation10 = new JRadioButtonMenuItem("10%"); + nudgeMutation.add(nudgeMutation10); + nudgeMutationButtonGroup.add(nudgeMutation10); + /* + JRadioButtonMenuItem nudgeMutation25 = new JRadioButtonMenuItem("25%"); + nudgeMutation.add(nudgeMutation25); + nudgeMutationButtonGroup.add(nudgeMutation25); + */ + + nudgeMutation0.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeMutationWeight = 0.0; + setLastX("" + nudgeMutationWeight, "NudgeMutationWeight", getSynthNameLocal(), false); + } + }); + + nudgeMutation1.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeMutationWeight = 0.01; + setLastX("" + nudgeMutationWeight, "NudgeMutationWeight", getSynthNameLocal(), false); + } + }); + + nudgeMutation2.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeMutationWeight = 0.02; + setLastX("" + nudgeMutationWeight, "NudgeMutationWeight", getSynthNameLocal(), false); + } + }); + + nudgeMutation5.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeMutationWeight = 0.05; + setLastX("" + nudgeMutationWeight, "NudgeMutationWeight", getSynthNameLocal(), false); + } + }); + + nudgeMutation10.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeMutationWeight = 0.10; + setLastX("" + nudgeMutationWeight, "NudgeMutationWeight", getSynthNameLocal(), false); + } + }); + +/* + nudgeMutation25.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeMutationWeight = 0.25; + setLastX("" + nudgeMutationWeight, "NudgeMutationWeight", getSynthNameLocal(), false); + } + }); +*/ + double nudgeVal = getLastXAsDouble("NudgeMutationWeight", getSynthNameLocal(), 0.0, false); + if (nudgeVal < 0.01) { nudgeMutationWeight = 0.0; nudgeMutation0.setSelected(true); } + else if (nudgeVal < 0.02) { nudgeMutationWeight = 0.01; nudgeMutation0.setSelected(true); } + else if (nudgeVal < 0.05) { nudgeMutationWeight = 0.02; nudgeMutation2.setSelected(true); } + else if (nudgeVal < 0.10) { nudgeMutationWeight = 0.05; nudgeMutation5.setSelected(true); } + else { nudgeMutationWeight = 0.10; nudgeMutation10.setSelected(true); } +// else if (nudgeVal < 0.25) { nudgeMutationWeight = 0.10; nudgeMutation10.setSelected(true); } +// else { nudgeMutationWeight = 0.25; nudgeMutation25.setSelected(true); } + + + + + ButtonGroup nudgeRecombinationButtonGroup = new ButtonGroup(); + JMenu nudgeRecombination = new JMenu("Set Nudge Recombination"); + nudgeMenu.add(nudgeRecombination); + JRadioButtonMenuItem nudgeRecombination2 = new JRadioButtonMenuItem("2%"); + nudgeRecombination.add(nudgeRecombination2); + nudgeRecombinationButtonGroup.add(nudgeRecombination2); + JRadioButtonMenuItem nudgeRecombination5 = new JRadioButtonMenuItem("5%"); + nudgeRecombination.add(nudgeRecombination5); + nudgeRecombinationButtonGroup.add(nudgeRecombination5); + JRadioButtonMenuItem nudgeRecombination10 = new JRadioButtonMenuItem("10%"); + nudgeRecombination.add(nudgeRecombination10); + nudgeRecombinationButtonGroup.add(nudgeRecombination10); + JRadioButtonMenuItem nudgeRecombination25 = new JRadioButtonMenuItem("25%"); + nudgeRecombination.add(nudgeRecombination25); + nudgeRecombinationButtonGroup.add(nudgeRecombination25); + JRadioButtonMenuItem nudgeRecombination50 = new JRadioButtonMenuItem("50%"); + nudgeRecombination.add(nudgeRecombination50); + nudgeRecombinationButtonGroup.add(nudgeRecombination50); + JRadioButtonMenuItem nudgeRecombination100 = new JRadioButtonMenuItem("100%"); + nudgeRecombination.add(nudgeRecombination100); + nudgeRecombinationButtonGroup.add(nudgeRecombination100); + + nudgeRecombination2.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeRecombinationWeight = 0.02; + setLastX("" + nudgeRecombinationWeight, "NudgeRecombinationWeight", getSynthNameLocal(), false); + } + }); + + nudgeRecombination5.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeRecombinationWeight = 0.05; + setLastX("" + nudgeRecombinationWeight, "NudgeRecombinationWeight", getSynthNameLocal(), false); + } + }); + + nudgeRecombination10.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeRecombinationWeight = 0.10; + setLastX("" + nudgeRecombinationWeight, "NudgeRecombinationWeight", getSynthNameLocal(), false); + } + }); + + nudgeRecombination25.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeRecombinationWeight = 0.25; + setLastX("" + nudgeRecombinationWeight, "NudgeRecombinationWeight", getSynthNameLocal(), false); + } + }); + + nudgeRecombination50.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeRecombinationWeight = 0.50; + setLastX("" + nudgeRecombinationWeight, "NudgeRecombinationWeight", getSynthNameLocal(), false); + } + }); + + nudgeRecombination100.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + nudgeRecombinationWeight = 1.0; + setLastX("" + nudgeRecombinationWeight, "NudgeRecombinationWeight", getSynthNameLocal(), false); + } + }); + + nudgeVal = getLastXAsDouble("NudgeRecombinationWeight", getSynthNameLocal(), 0.25, false); + if (nudgeVal < 0.05) { nudgeRecombinationWeight = 0.02; nudgeRecombination2.setSelected(true); } + else if (nudgeVal < 0.10) { nudgeRecombinationWeight = 0.05; nudgeRecombination5.setSelected(true); } + else if (nudgeVal < 0.25) { nudgeRecombinationWeight = 0.10; nudgeRecombination10.setSelected(true); } + else if (nudgeVal < 0.50) { nudgeRecombinationWeight = 0.25; nudgeRecombination25.setSelected(true); } + else if (nudgeVal < 1.00) { nudgeRecombinationWeight = 0.50; nudgeRecombination50.setSelected(true); } + else { nudgeRecombinationWeight = 1.00; nudgeRecombination100.setSelected(true); } + + + + + + + hillClimbMenu = new JMenuItem("Hill-Climb"); + menu.add(hillClimbMenu); + hillClimbMenu.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doHillClimb(); + } + }); + + menu.addSeparator(); + + menu.add(copyTab); + copyTab.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + SynthPanel p = findPanel(); + if (p != null) p.copyPanel(true); + } + }); + menu.add(pasteTab); + pasteTab.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + SynthPanel p = findPanel(); + if (p != null) + { + getUndo().push(getModel()); + getUndo().setWillPush(false); + setSendMIDI(false); + p.pastePanel(true); + setSendMIDI(true); + // We do this TWICE because for some synthesizers, updating a parameter + // will reveal other parameters which also must be updated but aren't yet + // in the mapping. + p.pastePanel(true); + getUndo().setWillPush(true); + } + } + }); + menu.add(copyMutableTab); + copyMutableTab.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + SynthPanel p = findPanel(); + if (p != null) p.copyPanel(false); + } + }); + menu.add(pasteMutableTab); + pasteMutableTab.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + SynthPanel p = findPanel(); + if (p != null) + { + getUndo().push(getModel()); + getUndo().setWillPush(false); + setSendMIDI(false); + p.pastePanel(false); + setSendMIDI(true); + // We do this TWICE because for some synthesizers, updating a parameter + // will reveal other parameters which also must be updated but aren't yet + // in the mapping. + p.pastePanel(false); + getUndo().setWillPush(true); + } + } + }); + resetTab.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + SynthPanel p = findPanel(); + if (p != null) + { + getUndo().push(getModel()); + getUndo().setWillPush(false); + p.resetPanel(); + getUndo().setWillPush(true); + } + } + }); + menu.add(resetTab); + + + menu.addSeparator(); + + editMutationMenu = new JMenuItem("Edit Mutation Parameters"); + menu.add(editMutationMenu); + editMutationMenu.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doToggleMutationMapEdit(); + } + }); + + JMenuItem clearAllMutationRestrictions = new JMenuItem("Clear All Mutation Parameters"); + menu.add(clearAllMutationRestrictions); + clearAllMutationRestrictions.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSetAllMutationMap(false); + } + }); + + JMenuItem setAllMutationRestrictions = new JMenuItem("Set All Mutation Parameters"); + menu.add(setAllMutationRestrictions); + setAllMutationRestrictions.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSetAllMutationMap(true); + } + }); + +/* + recombinationToggle = new JCheckBoxMenuItem("Use Parameters for Nudge/Merge"); + menu.add(recombinationToggle); + recombinationToggle.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + useMapForRecombination = !useMapForRecombination; + recombinationToggle.setSelected(useMapForRecombination); + setLastX("" + useMapForRecombination, "UseParametersForRecombination", getSynthNameLocal(), false); + } + }); + String recomb = getLastX("UseParametersForRecombination", getSynthNameLocal(), false); + if (recomb == null) recomb = "true"; + useMapForRecombination = Boolean.parseBoolean(recomb); + recombinationToggle.setSelected(useMapForRecombination); +*/ + + menu = new JMenu("MIDI"); + menubar.add(menu); + + + receiveCurrent = new JMenuItem("Request Current Patch"); + receiveCurrent.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(receiveCurrent); + receiveCurrent.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doRequestCurrentPatch(); + } + }); + + receivePatch = new JMenuItem("Request Patch..."); + menu.add(receivePatch); + receivePatch.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doRequestPatch(); + } + }); + + merge = new JMenu("Request Merge"); + menu.add(merge); + JMenuItem merge25 = new JMenuItem("Merge in 25%"); + merge.add(merge25); + JMenuItem merge50 = new JMenuItem("Merge in 50%"); + merge.add(merge50); + JMenuItem merge75 = new JMenuItem("Merge in 75%"); + merge.add(merge75); + JMenuItem merge100 = new JMenuItem("Merge in 100%"); + merge.add(merge100); + + merge25.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doRequestMerge(0.25); + } + }); + + merge50.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doRequestMerge(0.50); + } + }); + + merge75.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doRequestMerge(0.75); + } + }); + + merge100.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doRequestMerge(1.0); + } + }); + + menu.addSeparator(); + + + transmitCurrent = new JMenuItem("Send to Current Patch"); + menu.add(transmitCurrent); + transmitCurrent.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSendToCurrentPatch(); + } + }); + + transmitTo = new JMenuItem("Send to Patch..."); + menu.add(transmitTo); + transmitTo.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSendToPatch(); + } + }); + + transmitParameters = new JCheckBoxMenuItem("Sends Real Time Changes"); + menu.add(transmitParameters); + transmitParameters.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doAllowParameterTransmit(); + } + }); + + String sendInRealTime = getLastX("AllowTransmitParameters", getSynthNameLocal(), false); + if (sendInRealTime == null) sendInRealTime = "true"; + allowsTransmitsParameters = Boolean.parseBoolean(sendInRealTime); + transmitParameters.setSelected(allowsTransmitsParameters); + + + menu.addSeparator(); + + writeTo = new JMenuItem("Write to Patch..."); + writeTo.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_U, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(writeTo); + writeTo.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doWriteToPatch(); + } + }); + + + menu.addSeparator(); + + JMenuItem change = new JMenuItem("Change MIDI"); + menu.add(change); + change.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doChangeMIDI(); + } + }); + + JMenuItem disconnect = new JMenuItem("Disconnect MIDI"); + menu.add(disconnect); + disconnect.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doDisconnectMIDI(); + } + }); + + menu.addSeparator(); + + JMenuItem sendSysex = new JMenuItem("Send Sysex..."); + menu.add(sendSysex); + sendSysex.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + String hex = JOptionPane.showInputDialog(Synth.this, "Enter a sysex hex string", ""); + if (hex != null) + { + java.util.Scanner scanner = new java.util.Scanner(hex); + ArrayList list = new ArrayList(); + while(scanner.hasNextInt(16)) + { + list.add(new Integer(scanner.nextInt(16))); + } + byte[] data = new byte[list.size()]; + for(int i = 0; i < data.length; i++) + { + data[i] = (byte)(((Integer)(list.get(i))).intValue()); + } + tryToSendSysex(data); + } + } + }); + + testIncomingSynth = new JMenuItem("Report Next Synth MIDI"); + menu.add(testIncomingSynth); + testIncomingSynth.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + testIncomingSynthMIDI = !testIncomingSynthMIDI; + if (testIncomingSynthMIDI) + testIncomingSynth.setText("Stop Reporting Synth MIDI"); + else + testIncomingSynth.setText("Report Next Synth MIDI"); + } + }); + + testIncomingController = new JMenuItem("Report Next Controller MIDI"); + menu.add(testIncomingController); + testIncomingController.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + testIncomingControllerMIDI = !testIncomingControllerMIDI; + if (testIncomingControllerMIDI) + testIncomingController.setText("Stop Reporting Controller MIDI"); + else + testIncomingController.setText("Report Next Controller MIDI"); + } + }); + + menu.addSeparator(); + + JMenuItem allSoundsOff = new JMenuItem("Send All Sounds Off"); + menu.add(allSoundsOff); + allSoundsOff.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSendAllSoundsOff(false); + } + }); + + JMenuItem testNote = new JMenuItem("Send Test Note"); + testNote.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(testNote); + testNote.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSendTestNote(false); + } + }); + + testNotes = new JCheckBoxMenuItem("Send Test Notes"); + testNotes.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK)); + menu.add(testNotes); + testNotes.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSendTestNotes(); + } + }); + + menu.addSeparator(); + + sendTestNotesTimer = new javax.swing.Timer(1000, new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + if (hillClimbing) + hillClimb.updateSound(); + doSendTestNote(hillClimbing); + if (hillClimbing) + hillClimb.postUpdateSound(); + } + }); + sendTestNotesTimer.setRepeats(true); + + ButtonGroup testNoteGroup = new ButtonGroup(); + JRadioButtonMenuItem tns[] = new JRadioButtonMenuItem[7]; + + JMenu testNoteLength = new JMenu("Test Note Length"); + menu.add(testNoteLength); + JRadioButtonMenuItem tn = tns[0] = new JRadioButtonMenuItem("1/8 Second"); + testNoteLength.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteLength(125); + setLastX("" + getTestNoteLength(), "TestNoteLength", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[1] = new JRadioButtonMenuItem("1/4 Second"); + testNoteLength.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteLength(250); + setLastX("" + getTestNoteLength(), "TestNoteLength", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[2] = new JRadioButtonMenuItem("1/2 Second"); + testNoteLength.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteLength(500); + setLastX("" + getTestNoteLength(), "TestNoteLength", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[3] = new JRadioButtonMenuItem("1 Second"); + testNoteLength.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteLength(1000); + setLastX("" + getTestNoteLength(), "TestNoteLength", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[4] = new JRadioButtonMenuItem("2 Seconds"); + testNoteLength.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteLength(2000); + setLastX("" + getTestNoteLength(), "TestNoteLength", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[5] = new JRadioButtonMenuItem("4 Seconds"); + testNoteLength.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteLength(4000); + setLastX("" + getTestNoteLength(), "TestNoteLength", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[6] = new JRadioButtonMenuItem("8 Seconds"); + testNoteLength.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteLength(8000); + setLastX("" + getTestNoteLength(), "TestNoteLength", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + + int v = getLastXAsInt("TestNoteLength", getSynthNameLocal(), 500, false); + switch(v) + { + case 125: + tns[0].setSelected(true); setTestNoteLength(v); break; + case 250: + tns[1].setSelected(true); setTestNoteLength(v); break; + case 500: + tns[2].setSelected(true); setTestNoteLength(v); break; + case 1000: + tns[3].setSelected(true); setTestNoteLength(v); break; + case 2000: + tns[4].setSelected(true); setTestNoteLength(v); break; + case 4000: + tns[5].setSelected(true); setTestNoteLength(v); break; + case 8000: + tns[6].setSelected(true); setTestNoteLength(v); break; + default: + tns[2].setSelected(true); setTestNoteLength(500); break; + } + + + + testNoteGroup = new ButtonGroup(); + tns = new JRadioButtonMenuItem[9]; + + JMenu TestNotePause = new JMenu("Pause Between Test Notes"); + menu.add(TestNotePause); + tn = tns[0] = new JRadioButtonMenuItem("Default"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(TEST_NOTE_PAUSE_DEFAULT); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[1] = new JRadioButtonMenuItem("0 Seconds"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(0); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[2] = new JRadioButtonMenuItem("1/8 Second"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(125); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[3] = new JRadioButtonMenuItem("1/4 Second"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(250); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[4] = new JRadioButtonMenuItem("1/2 Second"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(500); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[5] = new JRadioButtonMenuItem("1 Second"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(1000); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[6] = new JRadioButtonMenuItem("2 Seconds"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(2000); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + + tn = tns[7] = new JRadioButtonMenuItem("4 Seconds"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(4000); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[8] = new JRadioButtonMenuItem("8 Seconds"); + TestNotePause.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePause(8000); + setLastX("" + getTestNotePause(), "TestNotePause", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + + + v = getLastXAsInt("TestNotePause", getSynthNameLocal(), -1, false); + switch(v) + { + case TEST_NOTE_PAUSE_DEFAULT: + tns[0].setSelected(true); setTestNotePause(v); break; + case 0: + tns[1].setSelected(true); setTestNotePause(v); break; + case 125: + tns[2].setSelected(true); setTestNotePause(v); break; + case 250: + tns[3].setSelected(true); setTestNotePause(v); break; + case 500: + tns[4].setSelected(true); setTestNotePause(v); break; + case 1000: + tns[5].setSelected(true); setTestNotePause(v); break; + case 2000: + tns[6].setSelected(true); setTestNotePause(v); break; + case 4000: + tns[7].setSelected(true); setTestNotePause(v); break; + case 8000: + tns[8].setSelected(true); setTestNotePause(v); break; + default: + tns[0].setSelected(true); setTestNotePause(TEST_NOTE_PAUSE_DEFAULT); break; + } + + + JMenu testNotePitch = new JMenu("Test Note Pitch"); + menu.add(testNotePitch); + + tns = new JRadioButtonMenuItem[7]; + + testNoteGroup = new ButtonGroup(); + tn = tns[0] = new JRadioButtonMenuItem("3 Octaves Up"); + testNotePitch.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePitch(96); + setLastX("" + 96, "TestNotePitch", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[1] =new JRadioButtonMenuItem("2 Octaves Up"); + testNotePitch.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePitch(84); + setLastX("" + getTestNotePitch(), "TestNotePitch", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[2] =new JRadioButtonMenuItem("1 Octave Up"); + testNotePitch.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePitch(72); + setLastX("" + getTestNotePitch(), "TestNotePitch", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[3] =new JRadioButtonMenuItem("Middle C"); + tn.setSelected(true); + testNotePitch.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePitch(60); + setLastX("" + getTestNotePitch(), "TestNotePitch", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[4] =new JRadioButtonMenuItem("1 Octave Down"); + testNotePitch.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePitch(48); + setLastX("" + getTestNotePitch(), "TestNotePitch", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[5] =new JRadioButtonMenuItem("2 Octaves Down"); + testNotePitch.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePitch(36); + setLastX("" + getTestNotePitch(), "TestNotePitch", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[6] =new JRadioButtonMenuItem("3 Octaves Down"); + testNotePitch.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNotePitch(24); + setLastX("" + getTestNotePitch(), "TestNotePitch", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + + + v = getLastXAsInt("TestNotePitch", getSynthNameLocal(), 60, false); + switch(v) + { + case 96: + tns[0].setSelected(true); setTestNotePitch(v); break; + case 84: + tns[1].setSelected(true); setTestNotePitch(v); break; + case 72: + tns[2].setSelected(true); setTestNotePitch(v); break; + case 60: + tns[3].setSelected(true); setTestNotePitch(v); break; + case 48: + tns[4].setSelected(true); setTestNotePitch(v); break; + case 36: + tns[5].setSelected(true); setTestNotePitch(v); break; + case 24: + tns[6].setSelected(true); setTestNotePitch(v); break; + default: + //tns[3].setSelected(true); setTestNotePitch(60); break; + break; + } + + + JMenu testNoteVolume = new JMenu("Test Note Volume"); + menu.add(testNoteVolume); + + tns = new JRadioButtonMenuItem[5]; + + testNoteGroup = new ButtonGroup(); + tn = tns[0] = new JRadioButtonMenuItem("Full Volume"); + tn.setSelected(true); + testNoteVolume.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteVelocity(127); + setLastX("" + getTestNoteVelocity(), "TestNoteVelocity", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[1] = new JRadioButtonMenuItem("1/2 Volume"); + testNoteVolume.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteVelocity(64); + setLastX("" + getTestNoteVelocity(), "TestNoteVelocity", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[2] = new JRadioButtonMenuItem("1/4 Volume"); + testNoteVolume.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteVelocity(32); + setLastX("" + getTestNoteVelocity(), "TestNoteVelocity", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[3] = new JRadioButtonMenuItem("1/8 Volume"); + testNoteVolume.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteVelocity(16); + setLastX("" + getTestNoteVelocity(), "TestNoteVelocity", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + tn = tns[4] = new JRadioButtonMenuItem("1/16 Volume"); + testNoteVolume.add(tn); + tn.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setTestNoteVelocity(8); + setLastX("" + getTestNoteVelocity(), "TestNoteVelocity", getSynthNameLocal(), false); + } + }); + testNoteGroup.add(tn); + + + v = getLastXAsInt("TestNoteVelocity", getSynthNameLocal(), 127, false); + switch(v) + { + case 127: + tns[0].setSelected(true); setTestNoteVelocity(v); break; + case 64: + tns[1].setSelected(true); setTestNoteVelocity(v); break; + case 32: + tns[2].setSelected(true); setTestNoteVelocity(v); break; + case 16: + tns[3].setSelected(true); setTestNoteVelocity(v); break; + case 8: + tns[4].setSelected(true); setTestNoteVelocity(v); break; + default: + tns[0].setSelected(true); setTestNoteVelocity(127); break; + } + + sendsAllSoundsOffBetweenNotesMenu = new JCheckBoxMenuItem("Send All Sounds Off Before Note On"); + sendsAllSoundsOffBetweenNotesMenu.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doSendsAllSoundsOffBetweenNotes(); + } + }); + + menu.add(sendsAllSoundsOffBetweenNotesMenu); + + String str1 = getLastX("SendAllSoundsOffBetweenNotes", getSynthNameLocal(), false); + if (str1 == null) str1 = "false"; + sendsAllSoundsOffBetweenNotes = Boolean.parseBoolean(str1); + sendsAllSoundsOffBetweenNotesMenu.setSelected(sendsAllSoundsOffBetweenNotes); + + + + menu = new JMenu("Map"); + menubar.add(menu); + + learningMenuItem = new JMenuItem("Map Absolute CC / NRPN"); + learningMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(learningMenuItem); + learningMenuItem.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMapCC(CCMap.TYPE_ABSOLUTE_CC); + } + }); + + // learningMenuItem64 = new JMenuItem("Map Relative CC [64]"); + learningMenuItem64 = new JMenuItem("Map Relative CC"); + learningMenuItem64.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(learningMenuItem64); + learningMenuItem64.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMapCC(CCMap.TYPE_RELATIVE_CC_64); + } + }); + + /* + learningMenuItem0 = new JMenuItem("Map Relative CC [0]"); + learningMenuItem0.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_J, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(learningMenuItem0); + learningMenuItem0.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doMapCC(CCMap.TYPE_RELATIVE_CC_0); + } + }); + */ + menu.addSeparator(); + + JMenuItem clearAllCC = new JMenuItem("Clear all Mapped CCs"); + menu.add(clearAllCC); + clearAllCC.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doClearAllCC(); + } + }); + + perChannelCCsMenuItem = new JCheckBoxMenuItem("Do Per-Channel CCs"); + menu.add(perChannelCCsMenuItem); + perChannelCCsMenuItem.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doPerChannelCCs(perChannelCCsMenuItem.getState()); + } + }); + perChannelCCsMenuItem.setSelected(perChannelCCs); + + menu.addSeparator(); + + passThroughCCMenuItem = new JCheckBoxMenuItem("Pass Through All CCs"); + menu.add(passThroughCCMenuItem); + passThroughCCMenuItem.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doPassThroughCC(passThroughCCMenuItem.getState()); + } + }); + String val = getLastX("PassThroughCC", getSynthNameLocal(), false); + setPassThroughCC(val != null && val.equalsIgnoreCase("true")); + + + passThroughControllerMenuItem = new JCheckBoxMenuItem("Pass Through Controller MIDI"); + menu.add(passThroughControllerMenuItem); + passThroughControllerMenuItem.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doPassThroughController(passThroughControllerMenuItem.getState()); + } + }); + val = getLastX("PassThroughController", getSynthNameLocal(), true); + setPassThroughController(val == null || val.equalsIgnoreCase("true")); + + + menu.addSeparator(); + JMenuItem colorMenu = new JMenuItem("Change Color Scheme"); + menu.add(colorMenu); + colorMenu.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + setupColors(); + } + }); + + JMenuItem resetColorMenu = new JMenuItem("Reset Color Scheme"); + menu.add(resetColorMenu); + resetColorMenu.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + if (showSimpleConfirm("Reset Colors", + "Reset Color Scheme to Defaults?

Note: after resetting colors, currently
open windows may look scrambled,
but new windows will look correct.
")) + resetColors(); + } + }); + + + menu = new JMenu("Tabs"); + menubar.add(menu); + + + JMenuItem prev = new JMenuItem("Previous Tab"); + prev.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_OPEN_BRACKET, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(prev); + prev.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doPreviousTab(); + } + }); + + + JMenuItem next = new JMenuItem("Next Tab"); + next.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_CLOSE_BRACKET, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(next); + next.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doNextTab(); + } + }); + + + JMenuItem taba = new JMenuItem("Tab 1"); + taba.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_1, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(taba); + taba.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doTab(0); + } + }); + + taba = new JMenuItem("Tab 2"); + taba.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_2, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(taba); + taba.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doTab(1); + } + }); + + taba = new JMenuItem("Tab 3"); + taba.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_3, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(taba); + taba.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doTab(2); + } + }); + + taba = new JMenuItem("Tab 4"); + taba.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_4, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(taba); + taba.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doTab(3); + } + }); + + taba = new JMenuItem("Tab 5"); + taba.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_5, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(taba); + taba.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doTab(4); + } + }); + + taba = new JMenuItem("Tab 6"); + taba.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_6, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(taba); + taba.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doTab(5); + } + }); + + taba = new JMenuItem("Tab 7"); + taba.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_7, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(taba); + taba.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doTab(6); + } + }); + + taba = new JMenuItem("Tab 8"); + taba.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_8, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + menu.add(taba); + taba.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doTab(7); + } + }); + + // Set up Mac. See Mac.java. + if (Style.isMac()) + Mac.setup(this); + + // Handle About menu for non-Macs + if (Style.isWindows() || Style.isUnix()) + { + // right now the only thing under "Help" is + // the About menu, so it doesn't exist on the Mac, + // where the About menu is elsewhere. + JMenu helpMenu = new JMenu("Help"); + JMenuItem aboutMenuItem = new JMenuItem("About Edisyn"); + aboutMenuItem.addActionListener(new ActionListener() + { + public void actionPerformed( ActionEvent e) + { + doAbout(); + } + }); + helpMenu.add(aboutMenuItem); + menubar.add(helpMenu); + } + + frame.getContentPane().setLayout(new BorderLayout()); + frame.getContentPane().add(this, BorderLayout.CENTER); + + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.addWindowListener(new java.awt.event.WindowAdapter() { - if (tuple == null) return false; - Receiver receiver = tuple.out; - if (receiver == null) return false; - receiver.send(message, -1); - return true; + public void windowClosing(java.awt.event.WindowEvent windowEvent) + { + doCloseWindow(); + } + + public void windowActivated(WindowEvent e) + { + sendAllSoundsOff(); // not doSendAllSoundsOff(false) because we don't want to turn off the test notes + windowBecameFront(); + lastActiveWindow = frame; + } + + }); + + updateTitle(); + numOpenWindows++; + + tabChanged(); // so we reset the copy tab etc. menus + + frame.pack(); + return frame; + } + + + void doPerChannelCCs(boolean val) + { + if (showSimpleConfirm("Change Per-Channel CC Settings?", "This clears all CCs. Change your per-channel CC settings?")) + { + clearLearned(); + perChannelCCs = val; + setLastX("" + perChannelCCs, "PerChannelCC", getSynthNameLocal(), false); } else - return false; + { + // reset + perChannelCCsMenuItem.setState(!perChannelCCsMenuItem.getState()); + } } + + + void doRequestCurrentPatch() + { + if (tuple == null || tuple.out == null) + { + if (!setupMIDI()) + return; + } - + Synth.this.merging = 0.0; + performRequestCurrentDump(); + } + + /** Milliseconds in which we pause before sending a patch request. The reason for this is that + some synths respond so fast to a patch request that we don't have time to take down the gatherPatchInfo(...) + window. As a result when the response comes in, */ + public static final int PAUSE_BEFORE_PATCH_REQUEST = 50; + + void doRequestPatch() + { + if (tuple == null || tuple.out == null) + { + if (!setupMIDI()) + return; + } + + Model tempModel = buildModel(); + if (gatherPatchInfo("Request Patch", tempModel, false)) + { + Synth.this.merging = 0.0; + performRequestDump(tempModel, true); + } + } + + void doRequestMerge(double percentage) + { + if (tuple == null || tuple.out == null) + { + if (!setupMIDI()) + return; + } + + Model tempModel = buildModel(); + if (gatherPatchInfo("Request Merge", tempModel, false)) + { + Synth.this.merging = percentage; + performRequestDump(tempModel, false); + } + } + + void doSendPatch() + { + if (tuple == null || tuple.out == null) + { + if (!setupMIDI()) + return; + } + + sendAllParameters(); + } + + void doSendToPatch() + { + if (tuple == null || tuple.out == null) + { + if (!setupMIDI()) + return; + } + + if (gatherPatchInfo("Send Patch To...", getModel(), true)) + { + performChangePatch(getModel()); // do it first here, as opposed to doWritetoPatch, which does it at the end + sendAllParameters(); + } + } + + + void doSendToCurrentPatch() + { + if (tuple == null || tuple.out == null) + { + if (!setupMIDI()) + return; + } + sendAllParameters(); + } + + void doReset() + { + //if (!showSimpleConfirm("Reset", "Reset the parameters to initial values?")) + // return; + + setSendMIDI(false); + // because loadDefaults isn't wrapped in an undo, we have to + // wrap it manually here + undo.setWillPush(false); + Model backup = (Model)(model.clone()); + loadDefaults(); + undo.setWillPush(true); + if (!backup.keyEquals(getModel())) // it's changed, do an undo push + undo.push(backup); + setSendMIDI(true); + sendAllParameters(); + + // this last statement fixes a mystery. When I call Randomize or Reset on + // a Blofeld or on a Microwave, all of the widgets update simultaneously. + // But on a Blofeld Multi or Microwave Multi they update one at a time. + // I've tried a zillion things, even moving all the widgets from the Blofeld Multi + // into the Blofeld, and it makes no difference! For some reason the OS X + // repaint manager is refusing to coallesce their repaint requests. So I do it here. + repaint(); + } + + void doWriteToPatch() + { + if (tuple == null || tuple.out == null) + { + if (!setupMIDI()) + return; + } + + if (gatherPatchInfo("Write Patch To...", getModel(), true)) + { + writeAllParameters(getModel()); + } + } + + public void writeAllParameters(Model model) + { + performChangePatch(model); // we need to be at the start for the Oberheim Matrix 1000 + tryToSendMIDI(emitAll(model, false, false)); + performChangePatch(model); // do it at the end AND start here, as opposed to doSendtoPatch, which does it first. We need to be at the end for the Kawai K4. + } + + void doChangeMIDI() + { + if (!setupMIDI("Choose new MIDI devices to send to and receive from.", tuple)) + return; + } + + boolean noMIDIPause = false; + boolean sendingAllSoundsOff = false; + void doSendAllSoundsOff(boolean fromDoSendTestNotes) // used to break infinite loop fights with doSendTestNotes() + { + if (!fromDoSendTestNotes && sendingTestNotes) + { + sendingAllSoundsOff = true; + doSendTestNotes(); // turn off + sendingAllSoundsOff = false; + } + + if (!sendingAllSoundsOff) + { + sendAllSoundsOff(); + } + } + + + void sendAllSoundsOff() + { + noMIDIPause = true; + try + { + // do an all sounds off (some synths don't properly respond to all notes off) + for(int i = 0; i < 16; i++) + tryToSendMIDI(new ShortMessage(ShortMessage.CONTROL_CHANGE, i, 120, 0)); + // do an all notes off (some synths don't properly respond to all sounds off) + for(int i = 0; i < 16; i++) + tryToSendMIDI(new ShortMessage(ShortMessage.CONTROL_CHANGE, i, 123, 0)); + // Plus, for some synths that respond to neither , maybe we can turn off the current note, + // assuming the user hasn't changed it. + for(int i = 0; i < 16; i++) + for(int j = 0; j < TEST_NOTE_PITCHES.length; j++) + tryToSendMIDI(new ShortMessage(ShortMessage.NOTE_OFF, i, TEST_NOTE_PITCHES[j], 64)); + } + catch (InvalidMidiDataException e2) + { + e2.printStackTrace(); + } + noMIDIPause = false; + } - /** Attempts to send MIDI sysex. Returns false if (1) the data was empty or null (2) - synth has turned off the ability to send temporarily (3) the sysex message is not - valid (4) an error occurred when the receiver tried to send the data. */ - public boolean tryToSendSysex(byte[] data) - { - return tryToSendSysex(data, false); - } + + int testNoteLength = 500; + void setTestNoteLength(int val) + { + testNoteLength = val; + setTestNotePause(getTestNotePause()); // update in case it's default + } + + int getTestNoteLength() + { + return testNoteLength; + } + + static final int TEST_NOTE_PAUSE_DEFAULT = -1; + int testNotePause = TEST_NOTE_PAUSE_DEFAULT; + void setTestNotePause(int val) + { + testNotePause = val; + sendTestNotesTimer.setDelay(getTestNoteTotalLength()); + } + + int getTestNoteTotalLength() + { + if (getTestNotePause() == TEST_NOTE_PAUSE_DEFAULT) + { + int len = getTestNoteLength(); + int delay = (len <= 500 ? len * 2 : len + 500); + return delay; + } + else + { + return getTestNotePause() + getTestNoteLength(); + } + } + + int getTestNotePause() + { + return testNotePause; + } + + + boolean sendingTestNotes = false; + javax.swing.Timer sendTestNotesTimer; + + public void doSendTestNotes() + { + if (sendingTestNotes) + { + sendTestNotesTimer.stop(); + doSendAllSoundsOff(true); + sendingTestNotes = false; + testNotes.setSelected(false); + } + else + { + sendTestNotesTimer.start(); + sendingTestNotes = true; + testNotes.setSelected(true); + } + } + + public boolean isSendingTestNotes() + { + return sendingTestNotes; + } + + boolean allowsTransmitsParameters; - /** Attempts to send MIDI sysex, possibly forcing sending MIDI even if getSendMIDI() is false. - Returns false if (1) the data was empty or null (2) - synth has turned off the ability to send temporarily (3) the sysex message is not - valid (4) an error occurred when the receiver tried to send the data. */ - public boolean tryToSendSysex(byte[] data, boolean forceSendMIDI) + public boolean getAllowsTransmitsParameters() { - if (data == null || data.length == 0) - return false; + return allowsTransmitsParameters; + } + + void doAllowParameterTransmit() + { + allowsTransmitsParameters = transmitParameters.isSelected(); + setLastX("" + allowsTransmitsParameters, "AllowTransmitParameters", getSynthNameLocal(), false); + } + + boolean sendsAllSoundsOffBetweenNotes; + + public boolean getSendsAllSoundsOffBetweenNotes() + { + return sendsAllSoundsOffBetweenNotes; + } + + void doSendsAllSoundsOffBetweenNotes() + { + sendsAllSoundsOffBetweenNotes = sendsAllSoundsOffBetweenNotesMenu.isSelected(); + setLastX("" + sendsAllSoundsOffBetweenNotes, "SendAllSoundsOffBetweenNotes", getSynthNameLocal(), false); + } + + public static final int[] TEST_NOTE_PITCHES = new int[] { 96, 84, 72, 60, 48, 36, 24 }; + int testNote = 60; + void setTestNotePitch(int note) { testNote = note; } + public int getTestNotePitch() { return testNote; } + + /** Override this to customize the MIDI channel of the test note. */ + public int getTestNoteChannel() { return getChannelOut(); } + + int testNoteVelocity = 127; + void setTestNoteVelocity(int velocity) { testNoteVelocity = velocity; } + public int getTestNoteVelocity() { return testNoteVelocity; } + + void doSendTestNote(boolean restartTestNotesTimer) + { + doSendTestNote(getTestNotePitch(), false, restartTestNotesTimer); + } - if (forceSendMIDI || getSendMIDI()) + public void doSendTestNote(final int testNote, final boolean alwaysSendNoteOff, boolean restartTestNotesTimer) + { + final int channel = getTestNoteChannel(); + final int velocity = getTestNoteVelocity(); + try { - if (tuple == null) return false; - Receiver receiver = tuple.out; - if (receiver == null) return false; - try { SysexMessage m = new SysexMessage(data, data.length); - receiver.send(m, -1); return true; } - catch (InvalidMidiDataException e) { e.printStackTrace(); return false; } + // possibly clear all notes + if (getSendsAllSoundsOffBetweenNotes()) + sendAllSoundsOff(); + + // play new note + tryToSendMIDI(new ShortMessage(ShortMessage.NOTE_ON, channel, getTestNotePitch(), velocity)); + + // schedule a note off + final int myNoteOnTick = ++noteOnTick; + javax.swing.Timer noteTimer = new javax.swing.Timer(testNoteLength, new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + if (alwaysSendNoteOff || noteOnTick == myNoteOnTick) // no more note on messages + try + { + tryToSendMIDI(new ShortMessage(ShortMessage.NOTE_OFF, channel, getTestNotePitch(), velocity)); + noteOnTick = 0; + } + catch (Exception e3) + { + e3.printStackTrace(); + } + } + }); + noteTimer.setRepeats(false); + noteTimer.start(); + } + catch (Exception e2) + { + e2.printStackTrace(); + } + + // the purpose of the code below is that when we're hill-climbing we often take longer than the full + // second of the test notes timer to just get the data out and play. So here we submit our timer, + // then we tell the test notes timer to reset itself to exactly the same initial delay as our timer. + // This SHOULD put the test notes timer back in the queue AFTER our note-off timer so we have enough + // time to turn off the note before the test notes timer fires another note. + + if (restartTestNotesTimer) + { + sendTestNotesTimer.setInitialDelay(getTestNoteTotalLength() + getPauseBetweenHillClimbPlays()); + sendTestNotesTimer.restart(); + } + } + + void doMapCC(int type) + { + // has to be done first because doPassThroughCC(false) may turn it off + setLearningCC(!getLearningCC()); + learningType = type; + doPassThroughCC(false); + } + + void doClearAllCC() + { + if (showSimpleConfirm("Clear CCs", "Are you sure you want to clear all CCs?")) + clearLearned(); + } + + void doPreviousTab() + { + setCurrentTab(getCurrentTab() - 1); + } + + void doNextTab() + { + setCurrentTab(getCurrentTab() + 1); + } + + void doTab(int tab) + { + setCurrentTab(tab); + } + + void doNew() + { + instantiate(Synth.this.getClass(), getSynthNameLocal(), false, true, tuple); + } + + void doNewSynth(int synth) + { + String[] synthNames = getSynthNames(); + instantiate(synths[synth], synthNames[synth], false, true, tuple); + } + + Synth doDuplicateSynth() + { + Synth newSynth = instantiate(Synth.this.getClass(), getSynthNameLocal(), false, true, tuple); + newSynth.setSendMIDI(false); + boolean currentPush = newSynth.undo.getWillPush(); + newSynth.undo.setWillPush(false); + model.copyValuesTo(newSynth.model); + newSynth.undo.setWillPush(currentPush); + newSynth.setSendMIDI(true); + return newSynth; + } + + void doUndo() + { + setSendMIDI(false); + if (model.equals(undo.top())) + model = undo.undo(null); // don't push into the redo stack + model = undo.undo(model); + boolean currentPush = undo.getWillPush(); + undo.setWillPush(false); + model.updateAllListeners(); + undo.setWillPush(currentPush); + setSendMIDI(true); + sendAllParameters(); + } + + void doRedo() + { + setSendMIDI(false); + model = (Model)(undo.redo(getModel())); + boolean currentPush = undo.getWillPush(); + undo.setWillPush(false); + model.updateAllListeners(); + undo.setWillPush(currentPush); + setSendMIDI(true); + sendAllParameters(); + } + + void doQuit() + { + sendAllSoundsOff(); + simplePause(50); // maybe enough time to flush out the all sounds off notes? dunno + System.exit(0); + } + + /** Removes the in/out/key devices. */ + void doDisconnectMIDI() + { + if (tuple != null) + tuple.dispose(); + + tuple = null; + setSendMIDI(true); + updateTitle(); + } + + void doSetNudge(int i, Model model, String name) + { + nudge[i] = (Model)(model.clone()); + nudgeTowards[i].setText("Towards " + (i + 1) + ": " + name); + nudgeTowards[i + 4].setText("Away from " + (i + 1) + ": " + name); + } + + void doSetNudge(int i) + { + doSetNudge(i, getModel(), getPatchName(getModel())); + } + + Model getNudge(int i) + { + return nudge[i]; + } + + int lastNudge = -1; + + double nudgeRecombinationWeight = 0.25; + double nudgeMutationWeight = 0.10; + + void doNudge(int towards) + { + if (towards == -1) return; + + setSendMIDI(false); + undo.push(model); + if (towards < 4) + { + if (nudgeRecombinationWeight > 0.0) model.recombine(random, nudge[towards], getMutationKeys(), //useMapForRecombination ? getMutationKeys() : model.getKeys(), + nudgeRecombinationWeight); + if (nudgeMutationWeight > 0.0) model.mutate(random, getMutationKeys(), nudgeMutationWeight); } else - return false; + { + if (nudgeRecombinationWeight > 0.0) model.opposite(random, nudge[towards - 4], getMutationKeys(), //useMapForRecombination ? getMutationKeys() : model.getKeys(), + nudgeRecombinationWeight, true); + if (nudgeMutationWeight > 0.0) model.mutate(random, getMutationKeys(), nudgeMutationWeight); + } + revise(); // just in case + + setSendMIDI(true); + sendAllParameters(); + + // this last statement fixes a mystery. When I call Randomize or Reset on + // a Blofeld or on a Microwave, all of the widgets update simultaneously. + // But on a Blofeld Multi or Microwave Multi they update one at a time. + // I've tried a zillion things, even moving all the widgets from the Blofeld Multi + // into the Blofeld, and it makes no difference! For some reason the OS X + // repaint manager is refusing to coallesce their repaint requests. So I do it here. + repaint(); + + lastNudge = towards; + } + + double lastMutate = 0.0; + + void doMutate(double probability) + { + if (probability == 0.0) + return; + + setSendMIDI(false); + undo.setWillPush(false); + Model backup = (Model)(model.clone()); + + model.mutate(random, getMutationKeys(), probability); + revise(); // just in case + + undo.setWillPush(true); + if (!backup.keyEquals(getModel())) // it's changed, do an undo push + undo.push(backup); + setSendMIDI(true); + + sendAllParameters(); + + // this last statement fixes a mystery. When I call Randomize or Reset on + // a Blofeld or on a Microwave, all of the widgets update simultaneously. + // But on a Blofeld Multi or Microwave Multi they update one at a time. + // I've tried a zillion things, even moving all the widgets from the Blofeld Multi + // into the Blofeld, and it makes no difference! For some reason the OS X + // repaint manager is refusing to coallesce their repaint requests. So I do it here. + repaint(); + lastMutate = probability; } - - - static Synth instantiate(Class _class, String name, boolean throwaway, boolean setupMIDI, Midi.Tuple tuple) - { - try - { - Synth synth = (Synth)(_class.newInstance()); - if (!throwaway) - { - synth.sprout(); - JFrame frame = ((JFrame)(SwingUtilities.getRoot(synth))); - frame.setVisible(true); - if (setupMIDI) - synth.setupMIDI("Choose MIDI devices to send to and receive from.", tuple); - - // we call this here even though it's already been called as a result of frame.setVisible(true) - // because it's *after* setupMidi(...) and so it gives synths a chance to send - // a MIDI sysex message in response to the window becoming front. - synth.windowBecameFront(); - } - return synth; - } - catch (IllegalAccessException e2) - { - e2.printStackTrace(); - JOptionPane.showMessageDialog(null, "An error occurred while creating the synth editor for \n" + name, "Creation Error", JOptionPane.ERROR_MESSAGE); - } - catch (InstantiationException e2) - { - e2.printStackTrace(); - JOptionPane.showMessageDialog(null, "An error occurred while creating the synth editor for \n" + name, "Creation Error", JOptionPane.ERROR_MESSAGE); - } - return null; - } - - - public static final Class[] synths = new Class[] { Blofeld.class, BlofeldMulti.class, MicrowaveXT.class, MicrowaveXTMulti.class }; - public static final String[] synthNames = { "Waldorf Blofeld (Single)", "Waldorf Blofeld (Multi)", "Waldorf Microwave II/XT/XTk (Single)", "Waldorf Microwave II/XT/XTk (Multi)" }; - - public JFrame sprout() + + void doPassThroughCC(boolean val) { - JFrame frame = new JFrame(); - JMenuBar menubar = new JMenuBar(); - frame.setJMenuBar(menubar); - JMenu menu = new JMenu("File"); - menubar.add(menu); + setPassThroughCC(val); + } - JMenuItem _new = new JMenuItem("New " + getSynthName()); - _new.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); - menu.add(_new); - _new.addActionListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - instantiate(Synth.this.getClass(), getSynthName(), false, true, tuple); - } - }); + void doPassThroughController(boolean val) + { + setPassThroughController(val); + } - JMenu newSynth = new JMenu("New Synth..."); - menu.add(newSynth); - for(int i = 0; i < synths.length; i++) - { - final int _i = i; - JMenuItem synthMenu = new JMenuItem(synthNames[i]); - synthMenu.addActionListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - instantiate(synths[_i], synthNames[_i], false, true, tuple); - } - }); - newSynth.add(synthMenu); - } + /** Goes through the process of saving to a new sysex file and associating it with + the editor. */ + void doSaveAs() + { + doSaveAs(null); + } + /** Goes through the process of saving to a new sysex file and associating it with + the editor. */ + void doSaveAs(String filename) + { + FileDialog fd = new FileDialog((Frame)(SwingUtilities.getRoot(this)), "Save Patch to Sysex File...", FileDialog.SAVE); - JMenuItem open = new JMenuItem("Load..."); - open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); - menu.add(open); - open.addActionListener(new ActionListener() + if (filename != null) { - public void actionPerformed( ActionEvent e) + fd.setFile(reviseFileName(filename)); + String path = getLastDirectory(); + if (path != null) + fd.setDirectory(path); + } + else if (file != null) + { + fd.setFile(reviseFileName(file.getName())); + fd.setDirectory(file.getParentFile().getPath()); + } + else + { + if (getPatchName(getModel()) != null) + fd.setFile(reviseFileName(getPatchName(getModel()).trim() + ".syx")); + else + fd.setFile(reviseFileName("Untitled.syx")); + String path = getLastDirectory(); + if (path != null) + fd.setDirectory(path); + } + + disableMenuBar(); + fd.setVisible(true); + enableMenuBar(); + File f = null; // make compiler happy + FileOutputStream os = null; + if (fd.getFile() != null) + try + { + f = new File(fd.getDirectory(), ensureFileEndsWith(fd.getFile(), ".syx")); + os = new FileOutputStream(f); + os.write(flatten(emitAll((Model)null, false, true))); + os.close(); + file = f; + setLastDirectory(fd.getDirectory()); + } + catch (IOException e) // fail { - doOpen(); + showSimpleError("File Error", "An error occurred while saving to the file " + (f == null ? " " : f.getName())); + e.printStackTrace(); + } + finally + { + if (os != null) + try { os.close(); } + catch (IOException e) { } } - }); - menu.addSeparator(); + updateTitle(); + } - JMenuItem close = new JMenuItem("Close Window"); - close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); - menu.add(close); - close.addActionListener(new ActionListener() + + /** Goes through the process of saving to an existing sysex file associated with + the editor, else it calls doSaveAs(). */ + void doSave() + { + if (file == null) { - public void actionPerformed( ActionEvent e) + doSaveAs(); + } + else + { + FileOutputStream os = null; + try { - doCloseWindow(); + os = new FileOutputStream(file); + os.write(flatten(emitAll((Model)null, false, true))); + os.close(); } - }); - - menu.addSeparator(); - - JMenuItem save = new JMenuItem("Save"); - save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); - menu.add(save); - save.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) + catch (Exception e) // fail { - doSave(); + showSimpleError("File Error", "An error occurred while saving to the file " + file); + e.printStackTrace(); } - }); - - JMenuItem saveAs = new JMenuItem("Save As..."); - menu.add(saveAs); - saveAs.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) + finally { - doSaveAs(); + if (os != null) + try { os.close(); } + catch (IOException e) { } } - }); + } - menu.addSeparator(); + updateTitle(); + } -/* - JMenuItem resetToDefault = new JMenuItem("Reset to Default"); - menu.add(resetToDefault); - resetToDefault.addActionListener(new ActionListener() + + void doCloseWindow() + { + JFrame frame = (JFrame)(SwingUtilities.getRoot(this)); + if (frame == null || !frame.isDisplayable()) return; // we clicked multiple times on the close button + + else if (requestCloseWindow()) { - public void actionPerformed( ActionEvent e) - { - getModel().resetToDefaults(); + sendAllSoundsOff(); + + // get rid of MIDI connection + if (tuple != null) + tuple.dispose(); + tuple = null; + + frame.setVisible(false); + frame.dispose(); + + numOpenWindows--; + if (numOpenWindows <= 0) + { + Synth result = doNewSynthPanel(); + if (result == null) + { + System.exit(0); + } } - }); -*/ + } + } + + public byte[][] extractSysexFromMidFile(File f) + { + try + { + Sequence seq = MidiSystem.getSequence(f); + int count = 0; + + Track[] t = seq.getTracks(); + for(int i = 0; i < t.length; i++) + for(int j = 0; j < t[i].size(); j++) + if (t[i].get(j).getMessage() instanceof SysexMessage) + count++; + + byte[][] sysex = new byte[count][]; + + count = 0; + for(int i = 0; i < t.length; i++) + for(int j = 0; j < t[i].size(); j++) + if (t[i].get(j).getMessage() instanceof SysexMessage) + { + // irritatingly, the sysex data doesn't include the 0xF0 + byte[] data = ((SysexMessage)(t[i].get(j).getMessage())).getData(); + sysex[count] = new byte[data.length + 1]; + sysex[count][0] = (byte)0xF0; + System.arraycopy(data, 0, sysex[count], 1, data.length); + count++; + } + return sysex; + } + catch (Exception ex) + { + ex.printStackTrace(); + return null; + } + } - JMenuItem exportTo = new JMenuItem("Export Diff To Text..."); - menu.add(exportTo); - exportTo.addActionListener(new ActionListener() + + public byte[][] cutUpSysex(byte[] data) + { + ArrayList sysex = new ArrayList(); + for(int start = 0; start < data.length; start++) { - public void actionPerformed( ActionEvent e) + if (data[start] != (byte)0xF0) + return (byte[][])sysex.toArray(new byte[sysex.size()][]); + int end; + for(end = start + 1; end < data.length; end++) { - doExport(true); + if (data[end] == (byte)0xF7) + { + byte[] d = new byte[end - start + 1]; + System.arraycopy(data, start, d, 0, end - start + 1); + sysex.add(d); + break; + } } - }); + start = end; // we'll get a start++ in a second + } + byte[][] b = (byte[][])sysex.toArray(new byte[sysex.size()][]); + return b; + } - menu = new JMenu("MIDI"); - menubar.add(menu); - + + + boolean recognizeAnyForLocal(byte[][] data) + { + for(int i = 0; i < data.length; i++) + { + if (recognizeLocal(data[i])) + return true; + } + return false; + } + + int recognizeSynthForSysex(byte[] data) + { + for(int i = 0; i < synths.length; i++) + { + if (recognize(synths[i], data)) + return i; + } + return -1; + } + + int[] recognizeAnySynthForSysex(byte[][] data) + { + boolean[] recognized = new boolean[synths.length]; - JMenuItem receiveCurrent = new JMenuItem("Request Current Patch"); - receiveCurrent.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); - menu.add(receiveCurrent); - receiveCurrent.addActionListener(new ActionListener() + int lastSynth = 0; + for(int i = 0; i < data.length; i++) { - public void actionPerformed( ActionEvent e) + // a little caching + if (recognize(synths[lastSynth], data[i])) + { + recognized[lastSynth] = true; + continue; + } + + for(int j = 0; j < synths.length; j++) { - if (tuple == null || tuple.out == null) + if (recognize(synths[j], data[i])) { - if (!setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.")) - return; + recognized[j] = true; + lastSynth = j; + break; } - - tryToSendSysex(requestCurrentDump(null)); } - }); + } + + int count = 0; + for(int i = 0; i < recognized.length; i++) + if (recognized[i]) + count++; + + int[] result = new int[count]; + count = 0; + for(int i = 0; i < recognized.length; i++) + if (recognized[i]) + result[count++] = i; + + return result; + } + - JMenuItem receive = new JMenuItem("Request Patch..."); - //receive.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); - menu.add(receive); - receive.addActionListener(new ActionListener() + + /** Goes through the process of opening a file and loading it into this editor. + This does NOT open a new editor window -- it loads directly into this editor. */ + + //// doOpen used to be pretty simple when we just opened files with one patch in them. + //// Now it's complex. Here's the gist of it: + //// + //// The user chooses a file, and we open it. It can be a sysex file or it can be a MID file. + //// Either way, we break it into multiple sysex messages. + //// + //// 0. If we don't recognize ANY messages, then we let the user know. + //// 1. If there is only ONE sysex message, then we try to either load it INTERNALLY or EXTERNALLY as appropriate. + //// 2. Another possibility is that the file contains messages (perhaps among others) which are part + //// of a multi-message patch load facility for a synthesizer. For example, the TX81Z requires that TWO messages + //// be loaded instead of one. At present if this occurs we can only load from this file if its FIRST message + //// is one of these patches. So then we concatenate the entire message collection and send it on to the editor. + //// Otherwise we let the user know that we can't load this thing. + //// 3. A last possibility is that we have multiple messages in the file, possibly from multiple synthesizers (or editor types). + //// If we're MERGING, we discard any messages for synthesizers other than the local synthesizer, and display the remainder. + //// Otherwise we show all messages for all synthesizers we understand. If there is only one remaining message, we just + //// load it like #1. Otherwise we have the user pick a synthesizer and patch and we load that. The user can optionally + //// load locally or externally if it's an internal synthesizer -- otherwise he must only load externally. + + boolean doOpen(boolean merge) + { + boolean succeeded = false; + parsingForMerge = merge; + String[] synthNames = getSynthNames(); + + + //// FIRST we have the user choose a file + + FileDialog fd = new FileDialog((Frame)(SwingUtilities.getRoot(this)), "Load Sysex Patch File...", FileDialog.LOAD); + fd.setFilenameFilter(new FilenameFilter() { - public void actionPerformed( ActionEvent e) + public boolean accept(File dir, String name) { - if (tuple == null || tuple.out == null) - { - if (!setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.")) - return; - } - - Model tempModel = new Model(); - if (gatherInfo("Request Patch", tempModel)) - { - tryToSendSysex(requestDump(tempModel)); - } + return ensureFileEndsWith(name, ".syx").equals(name) || ensureFileEndsWith(name, ".SYX").equals(name) || ensureFileEndsWith(name, ".sysex").equals(name) || ensureFileEndsWith(name, ".mid").equals(name) || ensureFileEndsWith(name, ".MID").equals(name) || ensureFileEndsWith(name, ".midi").equals(name) || ensureFileEndsWith(name, ".MIDI").equals(name); } }); - - JMenu merge = new JMenu("Request Merge"); - menu.add(merge); - JMenuItem merge25 = new JMenuItem("Merge in 25%"); - merge.add(merge25); - JMenuItem merge50 = new JMenuItem("Merge in 50%"); - merge.add(merge50); - JMenuItem merge75 = new JMenuItem("Merge in 75%"); - merge.add(merge75); - - merge25.addActionListener(new ActionListener() + + if (file != null) { - public void actionPerformed( ActionEvent e) + fd.setFile(file.getName()); + fd.setDirectory(file.getParentFile().getPath()); + } + else + { + String path = getLastDirectory(); + if (path != null) + fd.setDirectory(path); + } + + disableMenuBar(); + fd.setVisible(true); + enableMenuBar(); + File f = null; // make compiler happy + FileInputStream is = null; + if (fd.getFile() != null) + try { - if (tuple == null || tuple.out == null) + f = new File(fd.getDirectory(), fd.getFile()); + + is = new FileInputStream(f); + if (f.length() > MAX_FILE_LENGTH) { - if (!setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.")) - return; + showSimpleError("File Error", "File is too large and cannot be loaded."); } - - doMerge(0.25); - } - }); + else + { + byte[][] data; + String filename = f.getName(); + if (filename.endsWith(".mid") || filename.endsWith(".MID") || filename.endsWith(".midi") || filename.endsWith(".MIDI")) + { + data = extractSysexFromMidFile(f); + is.close(); + } + else // sysex file + { + byte[] d = new byte[(int)f.length()]; + readFully(d, is); + is.close(); + data = cutUpSysex(d); + } + + if (data == null || data.length == 0) // wasn't sysex, or we couldn't cut it up right. Maybe someone still recognizes it. + { + showSimpleError("File Error", "File does not appear to contain sysex data."); + succeeded = false; + } + else // it's valid sysex. Do we recognize it? + { + final boolean local = recognizeAnyForLocal(data); + final int[] external = recognizeAnySynthForSysex(data); + boolean hasMultipleDumpsSynth = false; + for(int i = 0; i < external.length; i++) // yuck, O(nm) + { + for(int j = 0; j < data.length; j++) + { + if (getNumSysexDumpsPerPatch(synths[external[i]], data[j]) > 1) + { hasMultipleDumpsSynth = true; break; } + } + } + + //// Are there NO PATCHES WE RECOGNIZE here? + + if (!local && external.length == 0) + { + succeeded = unknownSysexFileError(data[0]); + } + + + //// Is there JUST ONE PATCH WE RECOGNIZE HERE? + + else if (data.length == 1) + { + succeeded = loadOne(data[0], external[0], local, merge, f, fd, false); + } + + + //// Do we have a synth that involves multiple sysex dumps per patch? Irritating TX81Z + + else if (hasMultipleDumpsSynth) + { + int rec = recognizeSynthForSysex(data[0]); + if (rec < 1) // dunno what it is + { + String val = Midi.getManufacturerForSysex(data[0]); + + if (val == null) + showSimpleError("Patch Error", "File contains sysex for a synthesizer which uses multiple dumps\n" + + "for a single patch. In this case, Edisyn can only load the first\n" + + "dump, but it has an invalid manufacturer ID."); + else + showSimpleError("Patch Error", "File contains sysex for a synthesizer which uses multiple dumps\n" + + "for a single patch. In this case, Edisyn can only load the first\n" + + "dump, but it doesn't know how to load this one. The first dump\n" + + "appears to be by the following manufcaturer:\n" + val); + succeeded = false; + } + else + loadOne(flatten(data), rec, recognizeLocal(data[0]), merge, f, fd, false); + } - merge50.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) - { - if (tuple == null || tuple.out == null) - { - if (!setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.")) - return; - } - - doMerge(0.50); - } - }); - merge75.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) - { - if (tuple == null || tuple.out == null) - { - if (!setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.")) - return; + + //// We have multiple patches, possibly from multiple synths + + else // yuck, O(nm or worse) + { + succeeded = true; // we check for this later + Synth otherSynth = null; + + + //// ARE WE MERGING? + + if (merge) + { + if (!recognizeAnyForLocal(data)) + { + showSimpleError("Merge Error", "File contains multiple sysex patches, but none which can merge with this synthesizer."); + succeeded = false; + } + else + { + // primary + String[] sNames = new String[1]; + int[][] indices = new int[1][]; + String[][] pNames = new String[1][]; + sNames[0] = getSynthNameLocal(); + + int count = 0; + for(int i = 0; i < data.length; i++) + { + if (recognizeLocal(data[i])) + { + count++; + } + } + pNames[0] = new String[count]; + indices[0] = new int[count]; + + count = 0; + for(int i = 0; i < data.length; i++) + { + if (recognizeLocal(data[i])) + { + if (recognizeBulkLocal(data[i])) + { + pNames[0][count] = "" + count + " Bank Sysex"; + } + else + { + // build the synth to parse, then parse it and extract the name + if (otherSynth == null || otherSynth.getClass() != this.getClass()) + otherSynth = (Synth)(instantiate(Synth.this.getClass(), getSynthNameLocal(), true, false, null)); + otherSynth.printRevised = false; + otherSynth.setSendMIDI(false); + otherSynth.undo.setWillPush(false); + otherSynth.getModel().clearListeners(); // otherwise we GC horribly..... + otherSynth.parse(data[i], true); + pNames[0][count] = "" + count + " " + otherSynth.getPatchName(otherSynth.model); + } + + indices[0][count] = i; + count++; + } + } + + // build a blank two-level menu + TwoLevelMenu menu = new TwoLevelMenu(sNames, pNames, "Synth", "Patch", 0, 0); + + Color color = new JPanel().getBackground(); + HBox hbox = new HBox(); + hbox.setBackground(color); + VBox vbox = new VBox(); + vbox.setBackground(color); + vbox.add(new JLabel(" ")); + vbox.add(new JLabel("Choose a patch to merge.")); + vbox.add(new JLabel(" ")); + hbox.addLast(vbox); + vbox = new VBox(); + vbox.setBackground(color); + vbox.add(hbox); + vbox.add(menu); + + disableMenuBar(); + int result = JOptionPane.showOptionDialog(this, vbox, "Choose Patch from File", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, new Object[] { "Merge", "Cancel" }, "Merge"); + enableMenuBar(); + + if (result == 1) // cancel? + { + succeeded = false; + } + else + { + succeeded = loadOneLocal(data[indices[0][menu.getSecondary()]], merge, f, fd); + } + } + } + + + //// NOT Merging and multiple elements + + else + { + // primary + String[] sNames = new String[external.length]; + int[][] indices = new int[external.length][]; + String[][] pNames = new String[external.length][]; + for(int j = 0; j < external.length; j++) + { + sNames[j] = synthNames[external[j]]; + + int count = 0; + for(int i = 0; i < data.length; i++) + { + if (recognize(synths[external[j]], data[i])) + { + count++; + } + } + pNames[j] = new String[count]; + indices[j] = new int[count]; + + count = 0; + for(int i = 0; i < data.length; i++) + { + if (recognize(synths[external[j]], data[i])) + { + if (recognizeBulk(synths[external[j]], data[i])) + { + pNames[j][count] = "" + count + " Bank Sysex"; + } + else + { + // build the synth to parse, then parse it and extract the name + if (otherSynth == null || otherSynth.getClass() != synths[external[j]]) + otherSynth = (Synth)(instantiate(synths[external[j]], synthNames[external[j]], true, false, null)); + otherSynth.printRevised = false; + otherSynth.setSendMIDI(false); + otherSynth.undo.setWillPush(false); + otherSynth.getModel().clearListeners(); // otherwise we GC horribly..... + otherSynth.parse(data[i], true); + pNames[j][count] = "" + count + " " + otherSynth.getPatchName(otherSynth.model); + } + indices[j][count] = i; + count++; + } + } + + } + + if (sNames.length == 1 && pNames[0].length == 1) + { + succeeded = loadOne(data[indices[0][0]], external[0], synths[external[0]] == this.getClass(), merge, f, fd, false); + } + else + { + final JButton localButton = new JButton("Load In This Editor"); + // build the two-level menu + TwoLevelMenu menu = new TwoLevelMenu(sNames, pNames, "Synth", "Patch", 0, 0) + { + public void selection(int primary, int secondary) + { + localButton.setEnabled(Synth.this.getClass() == synths[external[primary]]); + } + }; + localButton.setEnabled(Synth.this.getClass() == synths[external[0]]); + + Color color = new JPanel().getBackground(); + HBox hbox = new HBox(); + hbox.setBackground(color); + VBox vbox = new VBox(); + vbox.setBackground(color); + vbox.add(new JLabel(" ")); + vbox.add(new JLabel("Choose a patch.")); + vbox.add(new JLabel(" ")); + hbox.addLast(vbox); + vbox = new VBox(); + vbox.setBackground(color); + vbox.add(hbox); + vbox.add(menu); + + JOptionPane pane = new JOptionPane(vbox, JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, new Object[] { "Load in New Editor" , localButton, "Cancel" }, "Load in New Editor"); + + localButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + pane.setValue(localButton); + } + }); + + + JDialog dialog = pane.createDialog(this, "Choose Patch from File"); + disableMenuBar(); + dialog.show(); + enableMenuBar(); + + Object result = pane.getValue(); + + if (result != null && result.equals(localButton)) + { + succeeded = loadOne(data[indices[menu.getPrimary()][menu.getSecondary()]], + external[menu.getPrimary()], true, + merge, f, fd, true); + } + else if (result != null && result.equals("Load in New Editor")) + { + succeeded = loadOne(data[indices[menu.getPrimary()][menu.getSecondary()]], + external[menu.getPrimary()], false, + merge, f, fd, true); + } + else // cancelled in some way. An apparent bug in JOptionPane is such that if you press ESC, then -1 is returned by getValue(). It should be returning null. + { + succeeded = false; + } + } + } + } + } } - - doMerge(0.75); - } - }); - - JMenuItem transmit = new JMenuItem("Send Patch"); - menu.add(transmit); - transmit.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) + } + catch (Throwable e) // fail -- could be an Error or an Exception { - if (tuple == null || tuple.out == null) - { - if (!setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.")) - return; - } - - sendAllParameters(); + showSimpleError("File Error", "An error occurred while loading from the file."); + e.printStackTrace(); } - }); - - JMenuItem random = new JMenuItem("Randomize"); - menu.add(random); - random.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) + finally { - mutate(1.0); - sendAllParameters(); + if (is != null) + try { is.close(); } + catch (IOException e) { } } - }); + + updateTitle(); + parsingForMerge = false; + return succeeded; + } - JMenuItem reset = new JMenuItem("Reset"); - menu.add(reset); - reset.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) - { - loadDefaults(); - sendAllParameters(); - } - }); + + // Private function used by doOpen(...) to issue an error when Edisyn doesn't know how to parse + // the provided sysex data. + boolean unknownSysexFileError(byte[] data) + { + String val = Midi.getManufacturerForSysex(data); + + if (val == null) + showSimpleError("File Error", "File might contain sysex data but has an invalid manufacturer ID."); + else + showSimpleError("File Error", "File does not contain sysex data that Edisyn knows how to load.\n" + + "This appears to be sysex data from the following manufacturer:\n" + val); + return false; + } - menu.addSeparator(); + // Private function used by doOpen(...) to load either locally or externally. We provide + // enough information for both situations, but the LOCAL boolean determines what to do. + boolean loadOne(byte[] data, int synth, boolean local, boolean merge, File f, FileDialog fd, boolean dontVerifyExternal) + { + String[] synthNames = getSynthNames(); - JMenuItem burn = new JMenuItem("Write Patch..."); - burn.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); - menu.add(burn); - burn.addActionListener(new ActionListener() + if (local) + return loadOneLocal(data, merge, f, fd); + else { - public void actionPerformed( ActionEvent e) + if (dontVerifyExternal || showSimpleConfirm("Load Other Synth Patch Editor", + "File doesn't contain sysex data for the " + getSynthNameLocal() + + ".\nIt appears to contain data for the " + synthNames[synth] + + ".\nLoad for the " + synthNames[synth] + " instead?")) { - if (tuple == null || tuple.out == null) - { - if (!setupMIDI("You are disconnected. Choose MIDI devices to send to and receive from.")) - return; - } - - if (gatherInfo("Write Patch", getModel())) - { - tryToSendSysex(emit(getModel(), false)); - } + return loadOneExternal(data, synths[synth], synthNames[synth], f, fd); } - }); - - menu.addSeparator(); - - JMenuItem change = new JMenuItem("Change MIDI"); - menu.add(change); - change.addActionListener(new ActionListener() + else + { + return false; + } + } + } + + // Private function used by doOpen(...) to load locally + boolean loadOneLocal(byte[] data, boolean merge, File f, FileDialog fd) + { + boolean succeeded; + + setSendMIDI(false); + undo.setWillPush(false); + Model backup = (Model)(model.clone()); + if (merge) { - public void actionPerformed( ActionEvent e) + succeeded = merge(data, getMergeProbability()); + if (!succeeded) { - setupMIDI("Choose MIDI devices to send to and receive from."); + showSimpleError("File Error", "Could not read the patch."); } - }); - - JMenuItem disconnect = new JMenuItem("Disconnect MIDI"); - menu.add(disconnect); - disconnect.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) + else { - disconnectMIDI(); + undo.setWillPush(true); + if (!backup.keyEquals(getModel())) // it's changed, do an undo push + undo.push(backup); } - }); - - menu.addSeparator(); - - JMenuItem allSoundsOff = new JMenuItem("Send All Sounds Off"); - menu.add(allSoundsOff); - allSoundsOff.addActionListener(new ActionListener() + } + else { - public void actionPerformed( ActionEvent e) + int result = parse(data, true); + if (result == PARSE_FAILED) { - try - { - for(int i = 0; i < 16; i++) - tryToSendMIDI(new ShortMessage(ShortMessage.CONTROL_CHANGE, i, 120, 0)); - } - catch (InvalidMidiDataException e2) - { - e2.printStackTrace(); - } + showSimpleError("File Error", "Could not read the patch."); + succeeded = false; } - }); - - JMenuItem testNote = new JMenuItem("Send Test Note"); - testNote.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); - menu.add(testNote); - testNote.addActionListener(new ActionListener() - { - public void actionPerformed( ActionEvent e) + else // including PARSE_CANCELLED { - try + undo.setWillPush(true); + if (!backup.keyEquals(getModel())) // it's changed, do an undo push + undo.push(backup); + if (result == PARSE_SUCCEEDED) { - int channel = 1; - if (tuple != null) - channel = tuple.outChannel; - tryToSendMIDI(new ShortMessage(ShortMessage.NOTE_ON, channel - 1, 60, 127)); - Thread.currentThread().sleep(500); - tryToSendMIDI(new ShortMessage(ShortMessage.NOTE_OFF, channel - 1, 60, 127)); + file = f; } - catch (InterruptedException e2) - { - e2.printStackTrace(); - } - catch (InvalidMidiDataException e2) + setLastDirectory(fd.getDirectory()); + + if (result == PARSE_SUCCEEDED || result == PARSE_SUCCEEDED_UNTITLED) { - e2.printStackTrace(); + succeeded = true; } + else + succeeded = false; } - }); - - frame.getContentPane().add(this); - frame.pack(); - - frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - frame.addWindowListener(new java.awt.event.WindowAdapter() + } + + setSendMIDI(true); + + // this last statement fixes a mystery. When I call Randomize or Reset on + // a Blofeld or on a Microwave, all of the widgets update simultaneously. + // But on a Blofeld Multi or Microwave Multi they update one at a time. + // I've tried a zillion things, even moving all the widgets from the Blofeld Multi + // into the Blofeld, and it makes no difference! For some reason the OS X + // repaint manager is refusing to coallesce their repaint requests. So I do it here. + repaint(); + return succeeded; + } + + // Private function used by doOpen(...) to load externally + boolean loadOneExternal(byte[] data, Class synthClass, String synthName, File f, FileDialog fd) + { + Synth otherSynth = instantiate(synthClass, synthName, false, true, null); + otherSynth.setSendMIDI(false); + + int result = otherSynth.parse(data, true); + if (result == PARSE_FAILED) { - public void windowClosing(java.awt.event.WindowEvent windowEvent) - { - doCloseWindow(); - } + otherSynth.showSimpleError("File Error", "Could not read the patch."); + otherSynth.setSendMIDI(true); + return false; + } + + otherSynth.file = f; + otherSynth.setLastDirectory(fd.getDirectory()); - public void windowActivated(WindowEvent e) - { - windowBecameFront(); - } + otherSynth.setSendMIDI(true); - }); - - updateTitle(); - numOpenWindows++; + // this last statement fixes a mystery. When I call Randomize or Reset on + // a Blofeld or on a Microwave, all of the widgets update simultaneously. + // But on a Blofeld Multi or Microwave Multi they update one at a time. + // I've tried a zillion things, even moving all the widgets from the Blofeld Multi + // into the Blofeld, and it makes no difference! For some reason the OS X + // repaint manager is refusing to coallesce their repaint requests. So I do it here. + otherSynth.repaint(); - return frame; + if (otherSynth.getSendsParametersAfterLoad()) // we'll need to do this + otherSynth.sendAllParameters(); + + // we don't want this to look like it's succeeded + return false; // (result == PARSE_SUCCEEDED || result == PARSE_SUCCEEDED_UNTITLED); } - - /** Perform a patch merge */ - void doMerge(double probability) + + + /** Pops up at the start of the program to ask the user what synth he wants. */ + static Synth doNewSynthPanel() { - Model tempModel = new Model(); - if (gatherInfo("Request Merge", tempModel)) + JPanel p = new JPanel(); + p.setLayout(new BorderLayout()); + p.add(new JLabel(" "), BorderLayout.NORTH); + p.add(new JLabel("Select a Synthesizer to Edit:"), BorderLayout.CENTER); + p.add(new JLabel(" "), BorderLayout.SOUTH); + + JPanel p2 = new JPanel(); + p2.setLayout(new BorderLayout()); + p2.add(p, BorderLayout.NORTH); + String[] synthNames = getSynthNames(); + JComboBox combo = new JComboBox(synthNames); + combo.setMaximumRowCount(32); + + // Note: Java classdocs are wrong: if you set a selected item to null (or to something not in the list) + // it doesn't just not change the current selected item, it sets it to some blank item. + String synth = getLastSynth(); + if (synth != null) combo.setSelectedItem(synth); + p2.add(combo, BorderLayout.CENTER); + + // For some reason the "Cancel" option is the MIDDLE option + //disableMenuBar(); + int result = JOptionPane.showOptionDialog(null, p2, "Edisyn", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[] { "Run", "Quit", "Disconnected" }, "Run"); + //enableMenuBar(); + if (result == 1 || // cancel + result < 0) // window closed or ESC + return null; + else { - Synth.this.merging = probability; - tryToSendSysex(requestDump(tempModel)); + setLastSynth("" + combo.getSelectedItem()); + return instantiate(synths[combo.getSelectedIndex()], synthNames[combo.getSelectedIndex()], false, (result == 0), null); } } + void doPrefs() + { + // do nothing + } - boolean sendsAllParametersInBulk = false; - - /** Sets whether the synth sends its patch dump (TRUE) as one single sysex dump or by - sending multiple separate parameter change requests (FALSE). By default this is FALSE. */ - public void setSendsAllParametersInBulk(boolean val) { sendsAllParametersInBulk = val; } - - /** Returns whether the synth sends its patch dump (TRUE) as one single sysex dump or by - sending multiple separate parameter change requests (FALSE). By default this is FALSE. */ - public boolean getSendsAllParametersInBulk() { return sendsAllParametersInBulk; } - - /** Sends all the parameters in a patch to the synth. - If sendsAllParametersInBulk was set to TRUE, then this is done by sending - a single patch write to working memory, which may not be supported by all synths. - - Otherwise this is done by sending each parameter separately, which isn't as fast. - The default sends each parameter separately. - */ - public void sendAllParameters() + void doAbout() { - if (sendsAllParametersInBulk) - { - tryToSendSysex(emit(getModel(), true)); - } - else - { - String[] keys = getModel().getKeys(); - for(int i = 0; i < keys.length; i++) - { - tryToSendSysex(emit(keys[i])); - } - } + ImageIcon icon = new ImageIcon(Synth.class.getResource("gui/About.jpg")); + // JLabel picture = new JLabel(icon); + JFrame frame = new JFrame("About Edisyn"); + frame.getContentPane().setLayout(new BorderLayout()); + frame.getContentPane().setBackground(Color.BLACK); + frame.getContentPane().add(new JLabel(icon), BorderLayout.CENTER); + + JPanel pane = new JPanel() + { + public Insets getInsets() { return new Insets(10, 10, 10, 10); } + }; + pane.setBackground(Color.GRAY); + pane.setLayout(new BorderLayout()); + + JLabel edisyn = new JLabel("Edisyn"); + edisyn.setForeground(Color.BLACK); + edisyn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28)); + pane.add(edisyn, BorderLayout.WEST); + + JLabel about = new JLabel("Version " + Edisyn.VERSION + " by Sean Luke http://github.com/eclab/edisyn/"); + about.setForeground(Color.BLACK); + about.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + pane.add(about, BorderLayout.EAST); + + frame.add(pane, BorderLayout.SOUTH); + frame.pack(); + frame.setResizable(false); + frame.setLocationRelativeTo(null); + frame.setVisible(true); } - - /** Guarantee that the given filename ends with the given ending. */ - static String ensureFileEndsWith(String filename, String ending) + + void doToggleMutationMapEdit() { - // do we end with the string? - if (filename.regionMatches(false,filename.length()-ending.length(),ending,0,ending.length())) - return filename; - else return filename + ending; + setShowingMutation(!isShowingMutation()); } + + void doSetAllMutationMap(boolean val) + { + String title = "Clear All Mutation Parameters"; + String message = "Are you sure you want to make all parameters immutable?"; + + if (val) + { + title = "Set All Mutation Parameters"; + message = "Are you sure you want to make all parameters mutable?"; + } - - /** Goes through the process of saving to a new sysex file and associating it with - the editor. */ - public void doSaveAs() + if (showSimpleConfirm(title, message)) + { + String[] keys = getModel().getKeys(); + for(int i = 0; i < keys.length; i++) + { + mutationMap.setFree(keys[i], val); + } + } + + repaint(); + } + + + + + + + + + ////// HILL-CLIMBING + + HillClimb hillClimb; + boolean hillClimbing = false; + + void doHillClimb() { - FileDialog fd = new FileDialog((Frame)(SwingUtilities.getRoot(this)), "Save Patch to Sysex File...", FileDialog.SAVE); - if (file == null) + if (hillClimbing) { - fd.setFile("Untitled.syx"); + Component selected = tabs.getSelectedComponent(); + hillClimb.shutdown(); + tabs.remove(hillClimbPane); + hillClimbMenu.setText("Hill-Climb"); + if (selected == hillClimbPane) // we were in the hill-climb pane when this menu was selected + tabs.setSelectedIndex(0); + hillClimbing = false; } else { - fd.setFile(file.getName()); - fd.setDirectory(file.getParentFile().getPath()); + hillClimb.startHillClimbing(); + hillClimb.startup(); + hillClimbPane = addTab("Hill-Climb", hillClimb); + tabs.setSelectedComponent(hillClimbPane); + hillClimbMenu.setText("Stop Hill-Climbing"); + hillClimbing = true; } - fd.setVisible(true); - File f = null; // make compiler happy - FileOutputStream os = null; - if (fd.getFile() != null) - try + } + + + + + + + + + //////// BULK DOWNLOADING + + boolean incomingPatch = false; + int patchCounter = 0; + Model currentPatch = null; + Model finalPatch = null; + File patchDirectory = null; + javax.swing.Timer patchTimer = null; + + public int getBulkDownloadWaitTime() { return 500; } + + void doGetAllPatches() + { + if (patchTimer != null) + { + patchTimer.stop(); + patchTimer = null; + getAll.setText("Download Batch..."); + showSimpleMessage("Batch Download", "Batch download stopped." ); + } + else + { + // turn off hill-climbing + if (hillClimbing) + doHillClimb(); + + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("Select Directory for Patches"); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setAcceptAllFileFilterUsed(false); + + if (file != null) { - f = new File(fd.getDirectory(), ensureFileEndsWith(fd.getFile(), ".syx")); - os = new FileOutputStream(f); - os.write(emit((Model)null, false)); - os.close(); - file = f; - } - catch (IOException e) // fail + chooser.setCurrentDirectory(new File(file.getParentFile().getPath())); + } + else { - JOptionPane.showMessageDialog(this, "An error occurred while saving to the file " + (f == null ? " " : f.getName()), "File Error", JOptionPane.ERROR_MESSAGE); - e.printStackTrace(); + String path = getLastDirectory(); + if (path != null) + chooser.setCurrentDirectory(new File(path)); } - finally + disableMenuBar(); + if (chooser.showOpenDialog((Frame)(SwingUtilities.getRoot(this))) != JFileChooser.APPROVE_OPTION) { - if (os != null) - try { os.close(); } - catch (IOException e) { } + enableMenuBar(); + currentPatch = null; + return; } + enableMenuBar(); + patchDirectory = chooser.getSelectedFile(); + + currentPatch = buildModel(); + if (!gatherPatchInfo("Starting Patch", currentPatch, false)) + { currentPatch = null; return; } + + finalPatch = buildModel(); + if (!gatherPatchInfo("Ending Patch", finalPatch, false)) + { currentPatch = null; return; } + + // request patch - updateTitle(); - } - + getAll.setText("Stop Downloading Batch"); + Synth.this.merging = 0.0; + performRequestDump(currentPatch, true); + incomingPatch = false; + + // set timer to request further patches + patchTimer = new javax.swing.Timer(getBulkDownloadWaitTime(), + new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + if (incomingPatch && patchLocationEquals(getModel(), currentPatch)) + { + processCurrentPatch(); + /* + doSendTestNote(false); + simplePause(testNoteLength); + try { tryToSendMIDI(new ShortMessage(ShortMessage.NOTE_OFF, getTestNoteChannel(), testNote, getTestNoteVelocity())); } + catch (InvalidMidiDataException ex) { } + simplePause(testNotePause); + */ + requestNextPatch(); + } + else + { + System.err.println("Warning (Synth): Download of " + getPatchLocationName(currentPatch) + " failed. Trying again."); + Synth.this.merging = 0.0; + performRequestDump(currentPatch, true); + } + } + }); + patchTimer.start(); + } + } - /** Goes through the process of saving to an existing sysex file associated with - the editor, else it calls doSaveAs(). */ - public void doSave() + void requestNextPatch() { - if (file == null) + if (patchLocationEquals(currentPatch, finalPatch)) // we're done { - doSaveAs(); - } + patchTimer.stop(); + patchTimer = null; + getAll.setText("Download Batch..."); + showSimpleMessage("Batch Download", "Batch download finished." ); + } else { + currentPatch = getNextPatchLocation(currentPatch); + Synth.this.merging = 0.0; + performRequestDump(currentPatch, true); + incomingPatch = false; + } + } + + /** This tells Edisyn whether your synthesizer sends patches to Edisyn via a sysex patch dump + (as opposed to individual CC or NRPN messages as is done in synths such as the PreenFM2). + The default is TRUE, which is nearly always the case. */ + public boolean getReceivesPatchesInBulk() { return true; } + + void processCurrentPatch() + { + // process current patch + byte[] data = flatten(emitAll((Model)null, false, true)); + if (data != null && data.length > 0) + { + if (patchDirectory == null) { new RuntimeException("Nonexistent directory for handling dump patch loads").printStackTrace(); return; } // this shouldn't happen + String filename = getPatchLocationName(getModel()); + if (filename == null) filename = ""; + if (filename.length() > 0) filename = filename + "."; + String patchname = getPatchName(getModel()); + if (patchname != null && patchname.length() > 0) + filename = filename + getPatchName(getModel()); + filename = filename.trim(); + if (filename.length() == 0) + filename = "Patch" + patchCounter + ".syx"; + else + filename = filename + ".syx"; + + // substitute separators + filename = filename.replace('/', '-').replace('\\', '-'); + FileOutputStream os = null; + File f = null; try { - os = new FileOutputStream(file); - os.write(emit((Model)null, false)); - os.close(); + os = new FileOutputStream(f = new File(patchDirectory, filename)); + os.write(data); } - catch (Exception e) // fail + catch (IOException e) // fail { - JOptionPane.showMessageDialog(this, "An error occurred while saving to the file " + file, "File Error", JOptionPane.ERROR_MESSAGE); + patchTimer.stop(); + patchTimer = null; + getAll.setText("Download Batch..."); + showSimpleError("Batch Download Failed.", "An error occurred while saving to the file " + (f == null ? " " : f.getName())); e.printStackTrace(); } finally @@ -1008,232 +5693,401 @@ public void doSave() catch (IOException e) { } } } + } + + + /** By default this method says two patches are the same if they have the same + "bank" and "number": if both are missing the "bank" (or the "number") then the + "bank" (or "number") is assumed to be the same. You should not use + Integer.MIN_VALUE as either a bank or a number. + Override this if you need further customization. */ + public boolean patchLocationEquals(Model patch1, Model patch2) + { + int bank1 = patch1.get("bank", Integer.MIN_VALUE); + int number1 = patch1.get("number", Integer.MIN_VALUE); + int bank2 = patch2.get("bank", Integer.MIN_VALUE); + int number2 = patch2.get("number", Integer.MIN_VALUE); + return (bank1 == bank2 && number1 == number2); + } + + + + + + + ///////// CALLBACKS - updateTitle(); + + /** Called by the model to update the synth whenever a parameter is changed. + You would probably never call this method. */ + public void update(String key, Model model) + { + if (learning) + updateTitle(); + + if (allowsTransmitsParameters && getSendMIDI()) + { + if (tryToSendMIDI(emitAll(key, STATUS_UPDATING_ONE_PARAMETER))) + simplePause(getPauseAfterSendOneParameter()); + } } + + + + - /** Goes through the process of dumping to a text file. */ - public void doExport(boolean doDiff) + ////////// UTILITIES + + + ArrayList disabledMenus = null; + int disableCount; + /** Disables the menu bar. disableMenuBar() and enableMenuBar() work in tandem to work around + a goofy bug in OS X: you can't disable the menu bar and reenable it: it won't reenable + unless the application loses focus and regains it, and even then sometimes it won't work. + These functions work properly however. You want to disable and enable the menu bar because + in OS X the menu bar still functions even when in a modal dialog! Bad OS X Java errors. + */ + public void disableMenuBar() { - FileDialog fd = new FileDialog((Frame)(SwingUtilities.getRoot(this)), "Export Patch" + (doDiff ? " Difference" : "") + " As Text...", FileDialog.SAVE); - if (file == null) + if (disabledMenus == null) { - fd.setFile("Untitled.txt"); + disabledMenus = new ArrayList(); + disableCount = 0; + JMenuBar bar = ((JFrame)(SwingUtilities.getWindowAncestor(this))).getJMenuBar(); + for(int i = 0; i < bar.getMenuCount(); i++) + { + JMenu menu = bar.getMenu(i); + if (menu != null) + { + for(int j = 0; j < menu.getItemCount(); j++) + { + JMenuItem item = menu.getItem(j); + if (item != null && item.isEnabled()) + { + disabledMenus.add(item); + item.setEnabled(false); + } + } + } + } } else { - fd.setFile(file.getName()); - fd.setDirectory(file.getParentFile().getPath()); + disableCount++; + return; } - fd.setVisible(true); - File f = null; // make compiler happy - FileOutputStream os = null; - if (fd.getFile() != null) - try + } + + /** Enables the menu bar. disableMenuBar() and enableMenuBar() work in tandem to work around + a goofy bug in OS X: you can't disable the menu bar and reenable it: it won't reenable + unless the application loses focus and regains it, and even then sometimes it won't work. + These functions work properly however. You want to disable and enable the menu bar because + in OS X the menu bar still functions even when in a modal dialog! Bad OS X Java errors. + */ + public void enableMenuBar() + { + if (disableCount == 0) + { + for(int i = 0; i < disabledMenus.size(); i++) { - f = new File(fd.getDirectory(), ensureFileEndsWith(fd.getFile(), ".txt")); - os = new FileOutputStream(f); - PrintWriter pw = new PrintWriter(os); - getModel().print(pw, doDiff); - pw.close(); - file = f; - } - catch (IOException e) // fail + disabledMenus.get(i).setEnabled(true); + } + disabledMenus = null; + } + else + { + disableCount--; + } + } + + // Guarantee that the given filename ends with the given ending. + public static String ensureFileEndsWith(String filename, String ending) + { + // do we end with the string? + if (filename.regionMatches(false,filename.length()-ending.length(),ending,0,ending.length())) + return filename; + else return filename + ending; + } + + // Flattens a two-dimensional array to a one-dimensional array, + // stripping out the non-sysex elements + byte[] flatten(Object[] data) + { + if (data == null) + return null; + if (data.length == 0) + return new byte[0]; + if (data.length == 1 && data[0] instanceof byte[]) + return (byte[])(data[0]); + + // otherwise flatten + + int len = 0; + for(int i = 0; i < data.length; i++) + { + if (data[i] instanceof byte[]) { - JOptionPane.showMessageDialog(this, "An error occurred while exporting to the file " + (f == null ? " " : f.getName()), "File Error", JOptionPane.ERROR_MESSAGE); - e.printStackTrace(); + len += ((byte[])data[i]).length; } - finally + else if (data[i] instanceof javax.sound.midi.SysexMessage) { - if (os != null) - try { os.close(); } - catch (IOException e) { } + len += ((javax.sound.midi.SysexMessage)data[i]).getLength(); + } + } + + byte[] result = new byte[len]; + int start = 0; + for(int i = 0; i < data.length; i++) + { + if (data[i] instanceof byte[]) + { + byte[] b = (byte[])(data[i]); + System.arraycopy(b, 0, result, start, b.length); + start += b.length; + } + else if (data[i] instanceof javax.sound.midi.SysexMessage) + { + // For some reason, getData() doesn't include the 0xF0, even + // though getLength() considers the 0xF0. I don't know why. + result[start++] = (byte)0xF0; + + byte[] b = ((javax.sound.midi.SysexMessage)data[i]).getData(); + System.arraycopy(b, 0, result, start, b.length); + start += b.length; } + } + return result; + } - updateTitle(); + int getLastXAsInt(String slot, String synth, int defaultVal, boolean getFromSynthOnly) + { + String tnls = getLastX(slot, synth, getFromSynthOnly); + try + { + return Integer.parseInt(tnls); + } + catch (NumberFormatException e) + { + return defaultVal; + } + catch (NullPointerException e2) + { + return defaultVal; + } + } + + double getLastXAsDouble(String slot, String synth, double defaultVal, boolean getFromSynthOnly) + { + String tnls = getLastX(slot, synth, getFromSynthOnly); + try + { + return Double.parseDouble(tnls); + } + catch (NumberFormatException e) + { + return defaultVal; + } + catch (NullPointerException e2) + { + return defaultVal; + } + } + + static final char DEFAULT_SEPARATOR_REPLACEMENT = '_'; + public String reviseFileName(String name) + { + if (name == null) name = ""; + char[] chars = name.toCharArray(); + for(int i = 0; i < chars.length; i++) + { + if (chars[i] <= 32 || chars[i] >= 127 || + chars[i] == java.io.File.pathSeparatorChar || + chars[i] == java.io.File.separatorChar) + chars[i] = DEFAULT_SEPARATOR_REPLACEMENT; + } + return new String(chars); } - /** Override this as you see fit to do something special when your window becomes front. */ - public void windowBecameFront() - { - } + /** Return an extra pause (beyond the pause after sending all parameters) after playing a test sound while hill-climbing. */ + public int getPauseBetweenHillClimbPlays() + { + return 0; + } + + + - public void doCloseWindow() + + //// BANK SYSEX SUPPORT + + + // This returns one of: + // 1. 0 ... names.length - 1 [choice of name] + // 2. BANK_CANCELLED [cancelled] + // 3. BANK_SAVED [saved] + // 4. BANK_UPLOADED [uploaded to synth] + + // If the value is #1, then you have to edit or merge the patch, and return whatever is appropriate. + // If the value is BANK_CANCELLED, BANK_SAVED, or BANK_UPLOADED (all < 0), then you should return PARSE_FAILED + + public static final int BANK_CANCELLED = -1; + public static final int BANK_SAVED = -2; + public static final int BANK_UPLOADED = - 3; + + public static int getNumSysexDumpsPerPatch() { return 1; } + + public int showBankSysexOptions(byte[] data, String[] names) { - if (requestCloseWindow()) + while(true) { - if (tuple != null) - tuple.dispose(); - tuple = null; - - JFrame frame = (JFrame)(SwingUtilities.getRoot(this)); - if (frame != null) + Color color = new JPanel().getBackground(); + HBox hbox = new HBox(); + hbox.setBackground(color); + VBox vbox = new VBox(); + vbox.setBackground(color); + vbox.add(new JLabel(" ")); + if (isParsingForMerge()) { - frame.setVisible(false); - frame.dispose(); + vbox.add(new JLabel("A Bank Sysex has been received.")); } - frame = null; - - numOpenWindows--; - if (numOpenWindows <= 0) + else { - System.exit(0); + vbox.add(new JLabel("A Bank Sysex has been received. You can save the sysex to a file,")); + vbox.add(new JLabel("write the sysex to the synth, or edit a patch from the list below.")); + } + vbox.add(new JLabel(" ")); + hbox.addLast(vbox); + vbox = new VBox(); + vbox.setBackground(color); + vbox.add(hbox); + JComboBox box = new JComboBox(names); + box.setMaximumRowCount(25); + vbox.add(box); + + int result = 0; + if (isParsingForMerge()) + { + disableMenuBar(); + result = JOptionPane.showOptionDialog(this, vbox, "Bank Sysex Received", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[] { "Merge Patch", "Cancel" }, "Merge Patch"); + enableMenuBar(); + if (result != 0) result = 3; // make it a "cancel" + } + else + { + disableMenuBar(); + result = JOptionPane.showOptionDialog(this, vbox, "Bank Sysex Received", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[] { "Edit Patch" , "Save Bank", "Write Bank", "Cancel" }, "Edit Patch"); + enableMenuBar(); } - } - - } - /** Goes through the process of opening a file and loading it into this editor. - This does NOT open a new editor window -- it loads directly into this editor. */ - public void doOpen() - { - FileDialog fd = new FileDialog((Frame)(SwingUtilities.getRoot(this)), "Load Sysex Patch File...", FileDialog.LOAD); - fd.setFilenameFilter(new FilenameFilter() - { - public boolean accept(File dir, String name) + if (result == 3 || result < 0) // cancel. ESC and Close Box both return < 0 { - return ensureFileEndsWith(name, ".syx").equals(name); + return BANK_CANCELLED; } - }); + else if (result == 2) // write + { + if (showSimpleConfirm("Write Bank", "Are you sure you want to write\nthe whole bank to the synth?")) + { + if (tuple == null || tuple.out == null) + { + if (!setupMIDI()) + continue; + } + data[2] = (byte) getChannelOut(); + boolean send = getSendMIDI(); + setSendMIDI(true); + tryToSendSysex(data); + setSendMIDI(send); + return BANK_UPLOADED; + } + } + else if (result == 1) // save + { + FileDialog fd = new FileDialog((Frame)(SwingUtilities.getRoot(this)), "Save Bank to Sysex File...", FileDialog.SAVE); - if (file != null) - { - fd.setFile(file.getName()); - fd.setDirectory(file.getParentFile().getPath()); + if (getPatchName(getModel()) != null) + fd.setFile(reviseFileName(getPatchName(getModel()).trim() + ".syx")); + else + fd.setFile(reviseFileName("Untitled.syx")); + String path = getLastDirectory(); + if (path != null) + fd.setDirectory(path); + + disableMenuBar(); + fd.setVisible(true); + enableMenuBar(); + + File f = null; // make compiler happy + FileOutputStream os = null; + if (fd.getFile() != null) + try + { + f = new File(fd.getDirectory(), ensureFileEndsWith(fd.getFile(), ".syx")); + os = new FileOutputStream(f); + os.write(data); + os.close(); + setLastDirectory(fd.getDirectory()); + if (os != null) + try { os.close(); } + catch (IOException ex) { } + return BANK_SAVED; + } + catch (IOException e) // fail + { + showSimpleError("File Error", "An error occurred while saving to the file " + (f == null ? " " : f.getName())); + e.printStackTrace(); + if (os != null) + try { os.close(); } + catch (IOException ex) { } + continue; // try again + } + } + else if (result == 0) // edit or merge patch + { + return box.getSelectedIndex(); + } } + + } + + + /** Writes out all parameters to a text file. */ + void doSaveText() + { + FileDialog fd = new FileDialog((Frame)(SwingUtilities.getRoot(this)), "Write Patch to Text File...", FileDialog.SAVE); - boolean failed = true; + if (getPatchName(getModel()) != null) + fd.setFile(reviseFileName(getPatchName(getModel()).trim() + ".txt")); + else + fd.setFile(reviseFileName("Untitled.txt")); + String path = getLastDirectory(); + if (path != null) + fd.setDirectory(path); + disableMenuBar(); fd.setVisible(true); + enableMenuBar(); File f = null; // make compiler happy - FileInputStream is = null; + PrintWriter os = null; if (fd.getFile() != null) try { - f = new File(fd.getDirectory(), fd.getFile()); - - is = new FileInputStream(f); - byte[] data = new byte[getExpectedSysexLength()]; - - int val = is.read(data, 0, getExpectedSysexLength()); - is.close(); - - if (val != getExpectedSysexLength()) // uh oh - throw new RuntimeException("File too short"); - - setSendMIDI(false); - parse(data); - setSendMIDI(true); - - file = f; - } - catch (Throwable e) // fail -- could be an Error or an Exception + f = new File(fd.getDirectory(), ensureFileEndsWith(fd.getFile(), ".txt")); + os = new PrintWriter(new FileOutputStream(f)); + getModel().print(os); + os.close(); + setLastDirectory(fd.getDirectory()); + } + catch (IOException e) // fail { - JOptionPane.showMessageDialog(this, "An error occurred while loading to the file " + f, "File Error", JOptionPane.ERROR_MESSAGE); + showSimpleError("File Error", "An error occurred while saving to the file " + (f == null ? " " : f.getName())); e.printStackTrace(); } finally { - if (is != null) - try { is.close(); } - catch (IOException e) { } + if (os != null) + os.close(); } - - updateTitle(); - } - - - /** Perform a JOptionPane confirm dialog with MUTLIPLE widgets that the user can select. The widgets are provided - in the array WIDGETS, and each has an accompanying label in LABELS. Returns TRUE if the user performed - the operation, FALSE if cancelled. */ - public static boolean doMultiOption(JComponent root, String[] labels, JComponent[] widgets, String title, String message) - { - JPanel panel = new JPanel(); - - int max = 0; - JLabel[] jlabels = new JLabel[labels.length]; - for(int i = 0; i < labels.length; i++) - { - jlabels[i] = new JLabel(labels[i] + " ", SwingConstants.RIGHT); - int width = (int)(jlabels[i].getPreferredSize().getWidth()); - if (width > max) max = width; - } - - Box vbox = new Box(BoxLayout.Y_AXIS); - for(int i = 0; i < labels.length; i++) - { - jlabels[i].setPreferredSize(new Dimension( - max, (int)(jlabels[i].getPreferredSize().getHeight()))); - jlabels[i].setMinimumSize(jlabels[i].getPreferredSize()); - // for some reason this has to be set as well - jlabels[i].setMaximumSize(jlabels[i].getPreferredSize()); - Box hbox = new Box(BoxLayout.X_AXIS); - hbox.add(jlabels[i]); - hbox.add(widgets[i]); - vbox.add(hbox); - } - - panel.setLayout(new BorderLayout()); - panel.add(vbox, BorderLayout.SOUTH); - JPanel p = new JPanel(); - p.setLayout(new BorderLayout()); - p.add(new JLabel(" "), BorderLayout.NORTH); - p.add(new JLabel(message), BorderLayout.CENTER); - p.add(new JLabel(" "), BorderLayout.SOUTH); - panel.add(p, BorderLayout.NORTH); - return (JOptionPane.showConfirmDialog(root, panel, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null) == JOptionPane.OK_OPTION); - } - - - /** Pops up at the start of the program to ask the user what synth he wants. */ - public static Synth doNewSynthPanel() - { - JPanel p = new JPanel(); - p.setLayout(new BorderLayout()); - p.add(new JLabel(" "), BorderLayout.NORTH); - p.add(new JLabel("Select a Synthesizer to Edit"), BorderLayout.CENTER); - p.add(new JLabel(" "), BorderLayout.SOUTH); - - JPanel p2 = new JPanel(); - p2.setLayout(new BorderLayout()); - p2.add(p, BorderLayout.NORTH); - JComboBox combo = new JComboBox(synthNames); - p2.add(combo, BorderLayout.CENTER); - - int result = JOptionPane.showOptionDialog(null, p2, "Edisyn", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[] { "Run", "Cancel", "Disconnected" }, "Run"); - if (result == 1) return null; - else return instantiate(synths[combo.getSelectedIndex()], synthNames[combo.getSelectedIndex()], false, (result == 0), null); } - - public abstract String getDefaultResourceFileName(); - - public void loadDefaults() - { - InputStream stream = getClass().getResourceAsStream(getDefaultResourceFileName()); - if (stream != null) - { - try - { - byte[] data = new byte[getExpectedSysexLength()]; - int val = stream.read(data, 0, getExpectedSysexLength()); - setSendMIDI(false); - parse(data); - setSendMIDI(true); - } - catch (Exception e) - { - e.printStackTrace(); - } - finally - { - try { stream.close(); } - catch (IOException e) { } - } - } - else - { - System.err.println("Didn't Parse"); - } - } + } diff --git a/edisyn/SyxDiff.java b/edisyn/SyxDiff.java new file mode 100644 index 00000000..05b3cf36 --- /dev/null +++ b/edisyn/SyxDiff.java @@ -0,0 +1,53 @@ +/*** + Copyright 2017 by Sean Luke + Licensed under the Apache License version 2.0 +*/ + +package edisyn; + +import java.io.*; +import java.util.*; + +/** This little java file is a utility which prints out diffs of sysex files + for debugging. You provide three arguments: the two file names, and the + number of bytes to skip at the beginning. Thereafter the columns are: + + 1. Byte index + 2. Byte index if the zeroth and every consecutive eighth byte are skipped, Korg 8-bit packing style + 3. Byte value in first file + 4. Byte value in second file + 5. ASCII value in first file + 6. ASCII value in second file + 7. "<--" if the bytes differ +*/ + +public class SyxDiff + { + public static void main(String[] args) throws Exception + { + FileInputStream scan1 = new FileInputStream(new File(args[0])); + FileInputStream scan2 = new FileInputStream(new File(args[1])); + + int index = 0; + int foo = 0; + for(int i = 0; i < Integer.valueOf(args[2]); i++) + { scan1.read(); scan2.read(); } + + while(true) + { + int b1 = scan1.read(); + int b2 = scan2.read(); + if (b1 < 0 || b2 < 0) break; + + if (index % 8 == 1) foo--; + + System.out.println("" + index + "\t" + + (index % 8 == 0 ? "-" : foo) + "\t" + b1 + "\t" + b2 + "\t" + + (b1 > 31 && b1 < 127 ? (char)b1 : "?") + "\t" + + (b2 > 31 && b2 < 127 ? (char)b2 : "?") + "\t" + + (b1 == b2 ? "" : "\t<--")); + index++; + foo++; + } + } + } diff --git a/edisyn/Undo.java b/edisyn/Undo.java new file mode 100644 index 00000000..d9a319fc --- /dev/null +++ b/edisyn/Undo.java @@ -0,0 +1,110 @@ +/*** + Copyright 2017 by Sean Luke + Licensed under the Apache License version 2.0 + i +*/ + +package edisyn; +import java.util.*; + +public class Undo + { + public static final boolean debug = false; + + public ArrayDeque undo = new ArrayDeque(); + public ArrayDeque redo = new ArrayDeque(); + public Synth synth; + boolean willPush = true; + public void clear() + { + undo.clear(); + redo.clear(); + } + + public boolean shouldShowUndoMenu() { return !undo.isEmpty(); } + public boolean shouldShowRedoMenu() { return !redo.isEmpty(); } + + public Undo(Synth synth) { this.synth = synth; } + + public void setWillPush(boolean val) { willPush = val; } + public boolean getWillPush() { return willPush; } + public void push(Model obj) + { + if (!willPush) + { + return; + } + undo.push((Model)(obj.clone())); + redo.clear(); + synth.updateUndoMenus(); + if (debug) System.err.println("Debug (Undo): Pushed " + obj); + + if (debug) printStacks(); + } + + public Model top() + { + if (undo.isEmpty()) + return null; + else return undo.peekFirst(); + } + + void printStacks() + { + System.err.println("Debug (Undo):\nUNDO"); + Object[] o = undo.toArray(); + for(int i = 0; i < o.length; i++) + { + System.err.println("" + i + " " + o[i]); + } + System.err.println("\nREDO"); + o = redo.toArray(); + for(int i = 0; i < o.length; i++) + { + System.err.println("" + i + " " + o[i]); + } + + } + + public Model undo(Model current) + { + if (undo.isEmpty()) + { + if (debug) System.err.println("Debug (Undo): Empty Undo" + current); + synth.updateUndoMenus(); + return current; + } + if (current != null) + { + if (debug) System.err.println("Debug (Undo): Pushing on Redo" + current); + redo.push((Model)(current.clone())); + } + Model model = undo.pop(); + synth.updateUndoMenus(); + if (debug) System.err.println("Debug (Undo): Undo " + current + " to " + model + " Left: " + undo.size()); + + if (debug) printStacks(); + return model; + } + + public Model redo(Model current) + { + if (redo.isEmpty()) + { + if (debug) System.err.println("Debug (Undo): Empty Redo " + current); + synth.updateUndoMenus(); + return current; + } + if (current != null) + { + if (debug) System.err.println("Debug (Undo): Pushing on Undo" + current); + undo.push((Model)(current.clone())); + } + Model model = redo.pop(); + synth.updateUndoMenus(); + if (debug) System.err.println("Debug (Undo): Redo " + current + " to " + model + " Left: " + redo.size()); + if (debug) printStacks(); + + return model; + } + } diff --git a/edisyn/gui/About.jpg b/edisyn/gui/About.jpg new file mode 100644 index 00000000..cef251db Binary files /dev/null and b/edisyn/gui/About.jpg differ diff --git a/edisyn/gui/Category.java b/edisyn/gui/Category.java index 1a5235ae..1eeb0458 100644 --- a/edisyn/gui/Category.java +++ b/edisyn/gui/Category.java @@ -11,7 +11,7 @@ import javax.swing.border.*; import javax.swing.*; import java.awt.event.*; - +import java.util.*; /** A pretty container for widgets to categorize them @@ -19,20 +19,609 @@ @author Sean Luke */ -public class Category extends JComponent +public class Category extends JComponent implements Gatherable { Color color; + Synth synth; + + String preamble; + String distributePreamble; + boolean pasteable = false; + boolean distributable = false; + boolean sendsAllParameters = false; + Gatherable auxillary = null; + + MenuItem copy = new MenuItem("Copy Category"); + MenuItem paste = new MenuItem("Paste Category"); + MenuItem distribute = new MenuItem("Distribute"); + MenuItem copyFromMutable = new MenuItem("Copy Category (Mutation Parameters Only)"); + MenuItem pasteToMutable = new MenuItem("Paste Category (Mutation Parameters Only)"); + MenuItem distributeToMutable = new MenuItem("Distribute (Mutation Parameters Only)"); + MenuItem reset = new MenuItem("Reset Category"); + + public void makePasteable(String preamble) { pasteable = true; this.preamble = preamble; } + public void makeDistributable(String preamble) { distributable = true; this.distributePreamble = preamble; } + public void makeUnresettable() { reset.setEnabled(false); } + public void setSendsAllParameters(boolean val) { sendsAllParameters = val; } + public boolean getSendsAllParameters() { return sendsAllParameters; } + + /** Returns an auxillary component. Sometimes a Category is broken into two pieces + (see KawaiK5 Harmonics (DHG) category for example), and when we gather elements, + we want to gather from the auxillary as well. */ + public Gatherable getAuxillary() { return auxillary; } + /** Sets an auxillary component. Sometimes a Category is broken into two pieces + (see KawaiK5 Harmonics (DHG) category for example), and when we gather elements, + we want to gather from the auxillary as well. */ + public void setAuxillary(Gatherable comp) { auxillary = comp; } + + PopupMenu pop = new PopupMenu(); + + public boolean isPasteCompatible(String preamble) + { + String copyPreamble = synth.getCopyPreamble(); + String myPreamble = preamble; + if (copyPreamble == null) return false; + if (myPreamble == null) return false; + + return (pasteable && + reduceAllDigitsAfterPreamble(copyPreamble, "").equals(reduceAllDigitsAfterPreamble(myPreamble, ""))); + } + + boolean canDistributeKey() + { + String lastKey = synth.getModel().getLastKey(); + if (lastKey == null) return false; + + ArrayList components = new ArrayList(); + gatherAllComponents(components); + for(int i = 0; i < components.size(); i++) + { + if (components.get(i) instanceof HasKey) + { + HasKey nc = (HasKey)(components.get(i)); + String key = nc.getKey(); + if (key.equals(lastKey)) + return true; + } + } + return false; + } - public Category(String label, Color color) + void resetCategory() { + boolean currentMIDI = synth.getSendMIDI(); + if (sendsAllParameters) + { + synth.setSendMIDI(false); + } + + Synth other = Synth.instantiate(synth.getClass(), synth.getSynthNameLocal(), true, true, synth.tuple); + ArrayList components = new ArrayList(); + gatherAllComponents(components); + for(int i = 0; i < components.size(); i++) + { + if (components.get(i) instanceof HasKey) + { + HasKey nc = (HasKey)(components.get(i)); + String key = nc.getKey(); + if (synth.getModel().exists(key) && other.getModel().exists(key)) + { + if (synth.getModel().isString(key)) + { + synth.getModel().set(key, other.getModel().get(key, "")); + } + else + { + synth.getModel().set(key, other.getModel().get(key, 0)); + } + } + else + System.err.println("Key missing in model : " + key); + } + } + + if (sendsAllParameters) + { + synth.setSendMIDI(currentMIDI); + synth.sendAllParameters(); + } + // so we don't have independent updates in OS X + repaint(); + } + + void copyCategory(boolean includeImmutable) + { + String[] mutationKeys = synth.getMutationKeys(); + if (mutationKeys == null) mutationKeys = new String[0]; + HashSet mutationSet = new HashSet(Arrays.asList(mutationKeys)); + + ArrayList keys = new ArrayList(); + ArrayList components = new ArrayList(); + gatherAllComponents(components); + for(int i = 0; i < components.size(); i++) + { + if (components.get(i) instanceof HasKey) + { + HasKey nc = (HasKey)(components.get(i)); + String key = nc.getKey(); + if (mutationSet.contains(key) || includeImmutable) + keys.add(key); + } + } + synth.setCopyKeys(keys); + synth.setCopyPreamble(preamble); + } + + + void pasteCategory(boolean includeImmutable) + { + String copyPreamble = synth.getCopyPreamble(); + String myPreamble = preamble; + if (copyPreamble == null) return; + if (myPreamble == null) return; + + ArrayList copyKeys = synth.getCopyKeys(); + if (copyKeys == null || copyKeys.size() == 0) + return; // oops + + // First we need to map OUR keys + HashMap keys = new HashMap(); + ArrayList components = new ArrayList(); + gatherAllComponents(components); + for(int i = 0; i < components.size(); i++) + { + if (components.get(i) instanceof HasKey) + { + String key = (String)(((HasKey)(components.get(i))).getKey()); + String reduced = reducePreamble(key, myPreamble); + keys.put(reduced, key); + } + } + + boolean currentMIDI = synth.getSendMIDI(); + if (sendsAllParameters) + { + synth.setSendMIDI(false); + } + + String[] mutationKeys = synth.getMutationKeys(); + if (mutationKeys == null) mutationKeys = new String[0]; + HashSet mutationSet = new HashSet(Arrays.asList(mutationKeys)); + + // Now we change keys as appropriate + for(int i = 0; i < copyKeys.size(); i++) + { + String key = (String)(copyKeys.get(i)); + String reduced = reducePreamble(key, copyPreamble); + String mapped = (String)(keys.get(reduced)); + if (mapped != null) + { + Model model = synth.getModel(); + if (model.exists(mapped) && (mutationSet.contains(mapped) || includeImmutable)) + { + if (model.isString(mapped)) + { + model.set(mapped, model.get(key, model.get(mapped, ""))); + } + else + { + model.set(mapped, model.get(key, model.get(mapped, 0))); + } + } + else + System.err.println("Warning (Category) 2: Key didn't exist " + mapped); + } + else + System.err.println("Warning (Category) 2: Null mapping for " + key + " (reduced to " + reduced + ")"); + } + + synth.revise(); + + if (sendsAllParameters) + { + synth.setSendMIDI(currentMIDI); + synth.sendAllParameters(); + } + // so we don't have independent updates in OS X + repaint(); + } + + void distributeCategory(boolean includeImmutable) + { + Model model = synth.getModel(); + String lastKey = model.getLastKey(); + + if (lastKey != null) + { + boolean currentMIDI = synth.getSendMIDI(); + if (sendsAllParameters) + { + synth.setSendMIDI(false); + } + + String lastReduced = reduceAllDigitsAfterPreamble(lastKey, distributePreamble); + + String[] mutationKeys = synth.getMutationKeys(); + if (mutationKeys == null) mutationKeys = new String[0]; + HashSet mutationSet = new HashSet(Arrays.asList(mutationKeys)); + + // Now we change keys as appropriate + ArrayList components = new ArrayList(); + gatherAllComponents(components); + for(int i = 0; i < components.size(); i++) + { + if (components.get(i) instanceof HasKey) + { + HasKey nc = (HasKey)(components.get(i)); + String key = nc.getKey(); + String reduced = reduceAllDigitsAfterPreamble(key, distributePreamble); + + if (reduced.equals(lastReduced)) + { + if (model.exists(key) && (mutationSet.contains(key) || includeImmutable)) + { + if (model.isString(key)) + { + model.set(key, model.get(lastKey, model.get(key, ""))); + } + else + { + model.set(key, model.get(lastKey, model.get(key, 0))); + } + } + else + System.err.println("Warning (Category): Key didn't exist " + key); + } + else + System.err.println("Warning (Category): Null mapping for " + key + " (reduced to " + reduced + ")"); + } + } + + synth.revise(); + + if (sendsAllParameters) + { + synth.setSendMIDI(currentMIDI); + synth.sendAllParameters(); + } + } + // so we don't have independent updates in OS X + repaint(); + } + + final static int STATE_FIRST_NUMBER = 0; + final static int STATE_FIRST_STRING = 1; + final static int STATE_NUMBER = 2; + final static int STATE_FINISHED = 3; + + public static String reducePreamble(String name, String preamble) + { + if (!name.startsWith(preamble)) + { + System.err.println("Warning (Category): Key " + name + " doesn't start with " + preamble); + return name; + } + return reduceAllDigitsAfterPreamble(preamble, "") + name.substring(preamble.length()); + } + + public static String reduceAllDigitsAfterPreamble(String name, String preamble) + { + char[] n = name.toCharArray(); + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < preamble.length(); i++) + { + sb.append(n[i]); + } + + int state = STATE_FIRST_STRING; + for(int i = preamble.length(); i < n.length; i++) + { + if (state == STATE_FIRST_STRING) + { + if (Character.isDigit(n[i])) + { + state = STATE_FINISHED; + } + else + { + sb.append(n[i]); + } + } + else if (state == STATE_FINISHED) + { + if (!Character.isDigit(n[i])) + { + sb.append(n[i]); + } + } + } + return sb.toString(); + } + + /** This function removes the FIRST string of digits in a name after a preamble, returns the resulting name. */ + public static String reduceFirstDigitsAfterPreamble(String name, String preamble) + { + char[] n = name.toCharArray(); + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < preamble.length(); i++) + { + sb.append(n[i]); + } + + int state = STATE_FIRST_STRING; + for(int i = preamble.length(); i < n.length; i++) + { + if (state == STATE_FIRST_STRING) + { + if (Character.isDigit(n[i])) + { + state = STATE_NUMBER; + } + else + { + sb.append(n[i]); + } + } + else if (state == STATE_NUMBER) + { + if (!Character.isDigit(n[i])) + { + sb.append(n[i]); + state = STATE_FINISHED; + } + } + else // state == STATE_FINISHED + { + sb.append(n[i]); + } + } + return sb.toString(); + } + + /** This function removes the SECOND string of digits in a name after a preamble, returns the resulting name. */ + public static String reduceSecondDigitsAfterPreamble(String name, String preamble) + { + char[] n = name.toCharArray(); + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < preamble.length(); i++) + { + sb.append(n[i]); + } + + int state = STATE_FIRST_NUMBER; + for(int i = preamble.length(); i < n.length; i++) + { + if (state == STATE_FIRST_NUMBER) + { + if (!Character.isDigit(n[i])) + { + // add it and jump to next state + sb.append(n[i]); + state = STATE_FIRST_STRING; + } + else + { + sb.append(n[i]); + } + } + else if (state == STATE_FIRST_STRING) + { + if (Character.isDigit(n[i])) + { + // skip it and jump to next state + state = STATE_NUMBER; + } + else + { + sb.append(n[i]); + } + } + else if (state == STATE_NUMBER) + { + if (!Character.isDigit(n[i])) + { + // add it and jump to next state + sb.append(n[i]); + state = STATE_FINISHED; + } + } + else // state == STATE_FINISHED + { + sb.append(n[i]); + } + } + return sb.toString(); + } + + + /** If synth is non-null, then double-clicking on the category will select or deselect all the + components inside it for mutation purposes. */ + public Category(final Synth synth, String label, Color color) + { + this.synth = synth; setLayout(new BorderLayout()); this.color = color; setName(label); + + if (synth != null) + { + addMouseListener(new MouseAdapter() + { + public void mousePressed(MouseEvent e) + { + if (e.getY() < 20 && + (((e.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK) || + ((e.getModifiers() & InputEvent.SHIFT_MASK) == InputEvent.SHIFT_MASK))) + { + copy.setEnabled(pasteable); + copyFromMutable.setEnabled(pasteable); + paste.setEnabled(pasteable && isPasteCompatible(preamble)); + pasteToMutable.setEnabled(pasteable && isPasteCompatible(preamble)); + distribute.setEnabled(distributable && canDistributeKey()); + distributeToMutable.setEnabled(distributable && canDistributeKey()); + + // we add, then remove the popup because I've discovered (in the Korg Wavestation SR Sequence Editor) + // that if the popup is pre-added, then it takes quite a while to dynamically add or remove categories. + Category.this.add(pop); + pop.show(e.getComponent(), e.getX(), e.getY()); + Category.this.remove(pop); + } + } + public void mouseClicked(MouseEvent e) + { + if (synth.isShowingMutation()) + { + boolean inBorder = ( e.getPoint().y < getInsets().top); + if (e.getClickCount() == 2 && inBorder) + { + boolean turnOn = true; + ArrayList comps = new ArrayList(); + gatherAllComponents(comps); + for(int i = 0; i < comps.size(); i++) + { + if (comps.get(i) instanceof NumericalComponent) + { + NumericalComponent nc = (NumericalComponent)(comps.get(i)); + String key = nc.getKey(); + if (synth.mutationMap.isFree(key) && synth.getModel().getStatus(key) != Model.STATUS_IMMUTABLE) + { turnOn = false; break; } + } + } + + for(int i = 0; i < comps.size(); i++) + { + if (comps.get(i) instanceof NumericalComponent) + { + NumericalComponent nc = (NumericalComponent)(comps.get(i)); + String key = nc.getKey(); + if (synth.getModel().getStatus(key) != Model.STATUS_IMMUTABLE) + synth.mutationMap.setFree(key, turnOn); + } + } + repaint(); + } + } + } + }); + } + + pop.add(copy); + copy.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + copyCategory(true); + } + }); + + pop.add(paste); + paste.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + synth.getUndo().push(synth.getModel()); + synth.getUndo().setWillPush(false); + synth.setSendMIDI(false); + pasteCategory(true); + synth.setSendMIDI(true); + // We do this TWICE because for some synthesizers, updating a parameter + // will reveal other parameters which also must be updated but aren't yet + // in the mapping. + pasteCategory(true); + synth.getUndo().setWillPush(true); + } + }); + + pop.add(distribute); + distribute.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + synth.getUndo().push(synth.getModel()); + synth.getUndo().setWillPush(false); + distributeCategory(true); + synth.getUndo().setWillPush(true); + } + }); + + pop.addSeparator(); + + pop.add(copyFromMutable); + copyFromMutable.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + copyCategory(false); + } + }); + + pop.add(pasteToMutable); + pasteToMutable.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + synth.getUndo().push(synth.getModel()); + synth.getUndo().setWillPush(false); + synth.setSendMIDI(false); + pasteCategory(false); + synth.setSendMIDI(true); + // We do this TWICE because for some synthesizers, updating a parameter + // will reveal other parameters which also must be updated but aren't yet + // in the mapping. + pasteCategory(false); + synth.getUndo().setWillPush(true); + } + }); + + pop.add(distributeToMutable); + distributeToMutable.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + synth.getUndo().push(synth.getModel()); + synth.getUndo().setWillPush(false); + distributeCategory(false); + synth.getUndo().setWillPush(true); + } + }); + + pop.addSeparator(); + + pop.add(reset); + reset.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + synth.getUndo().push(synth.getModel()); + synth.getUndo().setWillPush(false); + resetCategory(); + synth.getUndo().setWillPush(true); + } + }); + + copy.setEnabled(false); + copyFromMutable.setEnabled(false); + paste.setEnabled(false); + pasteToMutable.setEnabled(false); + distribute.setEnabled(false); + distributeToMutable.setEnabled(false); + reset.setEnabled(true); + + //Category.this.add(pop); + } + + public Insets getInsets() + { + Insets insets = (Insets)(super.getInsets().clone()); + insets.bottom = 0; + return insets; } public void setName(String label) { - // here we're going to do a little hack. TitledBorder doesn't put the title // on the FAR LEFT of the line, so when we draw the border we get a little square // dot to the left of the title which looks really annoying. Rather than build a @@ -47,7 +636,7 @@ public void setName(String label) final boolean[] paintingBorder = new boolean[1]; - final MatteBorder matteBorder = new MatteBorder(Style.CATEGORY_STROKE_WIDTH, 0, 0, 0, color) + final MatteBorder matteBorder = new MatteBorder(Style.CATEGORY_STROKE_WIDTH(), 0, 0, 0, color) { public Insets getBorderInsets(Component c, Insets insets) { @@ -59,11 +648,11 @@ public Insets getBorderInsets(Component c, Insets insets) }; TitledBorder titledBorder = new TitledBorder( - matteBorder, //BorderFactory.createLineBorder(color, Style.CATEGORY_STROKE_WIDTH, true), + matteBorder, " " + label + " ", TitledBorder.LEFT, TitledBorder.TOP, - Style.CATEGORY_FONT, + Style.CATEGORY_FONT(), color) { public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) @@ -74,21 +663,36 @@ public void paintBorder(Component c, Graphics g, int x, int y, int width, int he } }; - Border b = BorderFactory.createCompoundBorder(Style.CATEGORY_BORDER, titledBorder); + Border b = BorderFactory.createCompoundBorder(Style.CATEGORY_BORDER(), titledBorder); setBorder(b); repaint(); } + public void gatherAllComponents(java.util.ArrayList list) + { + Component[] c = getComponents(); + for(int i = 0; i < c.length; i++) + { + list.add(c[i]); + if (c[i] instanceof Gatherable) + ((Gatherable)c[i]).gatherAllComponents(list); + } + if (auxillary != null) + { + auxillary.gatherAllComponents(list); + } + } + public void paintComponent(Graphics g) { Graphics2D graphics = (Graphics2D) g; - Synth.prepareGraphics(g); + Style.prepareGraphics(g); Rectangle rect = getBounds(); rect.x = 0; rect.y = 0; - graphics.setPaint(Style.BACKGROUND_COLOR); + graphics.setPaint(Style.BACKGROUND_COLOR()); graphics.fill(rect); } } diff --git a/edisyn/gui/CheckBox.java b/edisyn/gui/CheckBox.java index 1e66b31f..357a2037 100644 --- a/edisyn/gui/CheckBox.java +++ b/edisyn/gui/CheckBox.java @@ -38,36 +38,58 @@ public CheckBox(String label, Synth synth, String key) { this(label, synth, key, false); } - + + public boolean isFlipped() { return flipped; } + public void addToWidth(int val) - { - addToWidth = val; - } + { + addToWidth = val; + } public JCheckBox getCheckBox() { return check; } - public CheckBox(String label, Synth synth, String key, boolean flipped) + boolean enabled = true; + public void setEnabled(boolean val) + { + enabled = val; + updateBorder(); + } + + public void updateBorder() + { + super.updateBorder(); + if (synth.isShowingMutation()) + check.setEnabled(false); + else + check.setEnabled(true && enabled); + } + + public CheckBox(String label, final Synth synth, final String key, boolean flipped) { super(synth, key); this.flipped = flipped; check = new JCheckBox(label) - { - public Dimension getMinimumSize() - { - return getPreferredSize(); - } - public Dimension getPreferredSize() - { - Dimension d = super.getPreferredSize(); - d.width += addToWidth; - return d; - } - }; - check.setFont(Style.SMALL_FONT); - check.setBackground(Style.TRANSPARENT); - check.setForeground(Style.TEXT_COLOR); + { + public Dimension getMinimumSize() + { + return getPreferredSize(); + } + public Dimension getPreferredSize() + { + Dimension d = super.getPreferredSize(); + d.width += addToWidth; + return d; + } + }; + + check.setFont(Style.SMALL_FONT()); + check.setOpaque(false); + //check.setContentAreaFilled(false); + //check.setBorderPainted(false); + //check.setBackground(Style.TRANSPARENT); // creates bugs in Windows + check.setForeground(Style.TEXT_COLOR()); setMax(1); setMin(0); @@ -76,6 +98,20 @@ public Dimension getPreferredSize() setLayout(new BorderLayout()); add(check, BorderLayout.CENTER); + check.addMouseListener(new MouseAdapter() + { + public void mouseClicked(MouseEvent e) + { + if (synth.isShowingMutation()) + { + synth.mutationMap.setFree(key, !synth.mutationMap.isFree(key)); + // wrap the repaint in an invokelater because the dial isn't responding right + SwingUtilities.invokeLater(new Runnable() { public void run() { repaint(); } }); + } + } + }); + + check.addActionListener(new ActionListener() { public void actionPerformed( ActionEvent e) diff --git a/edisyn/gui/Chooser.java b/edisyn/gui/Chooser.java index ef169a72..5d6f3db7 100644 --- a/edisyn/gui/Chooser.java +++ b/edisyn/gui/Chooser.java @@ -28,15 +28,43 @@ For the Mac, the JComboBox is made small (JComponent.sizeVariant = small), but t public class Chooser extends NumericalComponent { JComboBox combo; + int addToWidth = 0; // The integers corresponding to each element in the JComboBox. int[] vals; + String[] labels; + ImageIcon[] icons; JLabel label = new JLabel("888", SwingConstants.LEFT) { public Insets getInsets() { return new Insets(0, 0, 0, 0); } }; + boolean callActionListener = true; + + public String map(int val) + { + return "" + combo.getItemAt(val); + } + + public void setCallActionListener(boolean val) + { + callActionListener = val; + } + + public void updateBorder() + { + super.updateBorder(); + if (combo != null && + combo.isEnabled() == synth.isShowingMutation()) // this part prevents us from repeatedly calling setEnabled()... which creates a repaint loop + { + if (synth.isShowingMutation()) + combo.setEnabled(false); + else + combo.setEnabled(true); + } + } + public void update(String key, Model model) { if (combo == null) return; // we're not ready yet @@ -55,106 +83,183 @@ public void update(String key, Model model) for(int i = 0; i < vals.length; i++) if (vals[i] == state) { + // This is due to a Java bug. + // Unlike other widgets (like JCheckBox), JComboBox calls + // the actionlistener even when you programmatically change + // its value. OOPS. + setCallActionListener(false); combo.setSelectedIndex(i); + setCallActionListener(true); return; } } public Insets getInsets() - { - if (Style.CHOOSER_INSETS == null) - return super.getInsets(); - else return Style.CHOOSER_INSETS; - } + { + if (Style.CHOOSER_INSETS() == null) + return super.getInsets(); + else if (Style.isWindows()) + return Style.CHOOSER_WINDOWS_INSETS(); + else + return Style.CHOOSER_INSETS(); + } - /** Creates a JComboBox with the given label, modifying the given key in the Style. - The elements in the box are given by elements, and their corresponding numerical - values in the model are given in vals. */ - //public Chooser(String _label, Synth synth, String key, String[] elements, int[] vals) - // { - // this(_label, synth, key, elements); - // System.arraycopy(vals, 0, this.vals, 0, vals.length); - // } - /** Creates a JComboBox with the given label, modifying the given key in the Style. The elements in the box are given by elements, and their corresponding numerical values in the model 0...n. */ public Chooser(String _label, Synth synth, String key, String[] elements) + { + this(_label, synth, key, elements, null); + } + + /** Creates a JComboBox with the given label, modifying the given key in the Style. + The elements in the box are given by elements, with images in icons, and their corresponding numerical + values in the model 0...n. Note that OS X won't behave properly with icons larger than about 34 high. */ + public Chooser(String _label, final Synth synth, final String key, String[] elements, ImageIcon[] icons) { super(synth, key); - label.setFont(Style.SMALL_FONT); - label.setBackground(Style.TRANSPARENT); - label.setForeground(Style.TEXT_COLOR); + label.setFont(Style.SMALL_FONT()); + label.setBackground(Style.BACKGROUND_COLOR()); // TRANSPARENT); + label.setForeground(Style.TEXT_COLOR()); //label.setMaximumSize(label.getPreferredSize()); - combo = new JComboBox(elements); + combo = new JComboBox(elements) + { + public Dimension getMinimumSize() + { + return getPreferredSize(); + } + public Dimension getPreferredSize() + { + Dimension d = super.getPreferredSize(); + d.width += addToWidth; + return d; + } + + protected void processMouseEvent(MouseEvent e) + { + super.processMouseEvent(e); + if (e.getID() == MouseEvent.MOUSE_CLICKED) + { + if (synth.isShowingMutation()) + { + synth.mutationMap.setFree(key, !synth.mutationMap.isFree(key)); + // wrap the repaint in an invokelater because the dial isn't responding right + SwingUtilities.invokeLater(new Runnable() { public void run() { repaint(); } }); + } + } + } + }; + combo.putClientProperty("JComponent.sizeVariant", "small"); combo.setEditable(false); - combo.setFont(Style.SMALL_FONT); - combo.setMaximumRowCount(32); + combo.setFont(Style.SMALL_FONT()); + combo.setMaximumRowCount(33); // 33, not 32, to accommodate modulation destinations for Matrix 1000 setElements(_label, elements); + this.icons = icons; + this.labels = elements; + if (icons != null) + { + combo.setRenderer(new ComboBoxRenderer()); + //if (Style.isMac()) + combo.putClientProperty("JComponent.sizeVariant", "regular"); + } + setState(getState()); setLayout(new BorderLayout()); add(combo, BorderLayout.CENTER); - add(label, BorderLayout.NORTH); - - // we don't use an actionlistener here because of a Java bug. - // Unlike other widgets (like JCheckBox), JComboBox calls - // the actionlistener even when you programmatically change - // its value. OOPS. + if (isLabelToLeft()) + add(label, BorderLayout.WEST); + else + add(label, BorderLayout.NORTH); + + + /// Apparent OS X Java bug: sometimes after you programmatically change + /// the value of a JComboBox, it no longer sends ActionListener events. :-( combo.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { - if (e.getStateChange() == ItemEvent.SELECTED) + // This is due to a Java bug. + // Unlike other widgets (like JCheckBox), JComboBox calls + // the actionlistener even when you programmatically change + // its value. OOPS. + if (callActionListener) { setState(combo.getSelectedIndex()); } } }); + +/* + combo.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + // This is due to a Java bug. + // Unlike other widgets (like JCheckBox), JComboBox calls + // the actionlistener even when you programmatically change + // its value. OOPS. + if (callActionListener) + { + setState(combo.getSelectedIndex()); + } + } + }); +*/ } + public boolean isLabelToLeft() { return false; } + + public void addToWidth(int val) + { + addToWidth = val; + } + public JComboBox getCombo() - { - return combo; - } - + { + return combo; + } + public String getElement(int position) - { - return (String)(combo.getItemAt(position)); - } - + { + return (String)(combo.getItemAt(position)); + } + public int getNumElements() - { - return combo.getItemCount(); - } - + { + return combo.getItemCount(); + } + public int getIndex() - { - return combo.getSelectedIndex(); - } - + { + return combo.getSelectedIndex(); + } + public void setIndex(int index) - { - combo.setSelectedIndex(index); - } - + { + setCallActionListener(false); + combo.setSelectedIndex(index); + setCallActionListener(true); + } + public void setLabel(String _label) - { + { label.setText(" " + _label); - } - + } + public void setElements(String _label, String[] elements) - { + { + setCallActionListener(false); label.setText(" " + _label); combo.removeAllItems(); for(int i = 0; i < elements.length; i++) - combo.addItem(elements[i]); + combo.addItem(elements[i]); vals = new int[elements.length]; for(int i = 0; i < vals.length; i++) @@ -162,19 +267,56 @@ public void setElements(String _label, String[] elements) setMin(0); setMax(elements.length - 1); + setCallActionListener(true); combo.setSelectedIndex(0); setState(combo.getSelectedIndex()); - } + revalidate(); + repaint(); + } - public void paintComponent(Graphics g) + class ComboBoxRenderer extends JLabel implements ListCellRenderer { - Graphics2D graphics = (Graphics2D) g; - - Rectangle rect = getBounds(); - rect.x = 0; - rect.y = 0; - graphics.setPaint(Style.BACKGROUND_COLOR); - graphics.fill(rect); + public ComboBoxRenderer() + { + setOpaque(true); + setHorizontalAlignment(CENTER); + setVerticalAlignment(CENTER); + } + + /* + * This method finds the image and text corresponding + * to the selected value and returns the label, set up + * to display the text and image. + */ + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) + { + // Get the selected index. (The index param isn't always valid, so just use the value.) + //int selectedIndex = ((Integer)value).intValue(); + + if (index == -1) index = combo.getSelectedIndex(); + if (index == -1) return this; + + if (isSelected) + { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } + else + { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + + // Set the icon and text. If icon was null, say so. + ImageIcon icon = icons[index]; + String label = labels[index]; + setIcon(icon); + setText(label); + setFont(list.getFont()); + return this; + } } + + } diff --git a/edisyn/gui/ColorWell.java b/edisyn/gui/ColorWell.java new file mode 100644 index 00000000..81793b8b --- /dev/null +++ b/edisyn/gui/ColorWell.java @@ -0,0 +1,61 @@ +/* + Copyright 2006 by Sean Luke and George Mason University + Licensed under the Academic Free License version 3.0 + See the file "LICENSE" for more information +*/ + +package edisyn.gui; +import java.awt.*; +import javax.swing.*; +import javax.swing.border.*; +import java.awt.event.*; + +public class ColorWell extends JPanel + { + Color color; + + public Insets getInsets() { return new Insets(4, 4, 4, 4); } + + public ColorWell() + { + this(new Color(0,0,0,0)); + } + + public ColorWell(Color c) + { + color = c; + addMouseListener(new MouseAdapter() + { + public void mouseReleased(MouseEvent e) + { + Color col = JColorChooser.showDialog(null, "Choose Color", color); + setColor(col); + } + }); + setBorder(BorderFactory.createEmptyBorder(0,0,0,0)); + } + + // maybe in the future we'll add an opacity mechanism + public void paintComponent(Graphics g) + { + g.setColor(color); + g.fillRect(0,0,getWidth(),getHeight()); + } + + public void setColor(Color c) + { + if (c != null) + color = changeColor(c); + repaint(); + } + + public Color getColor() + { + return color; + } + + public Color changeColor(Color c) + { + return c; + } + } diff --git a/edisyn/gui/CrossfadeEnvelopeDisplay.java b/edisyn/gui/CrossfadeEnvelopeDisplay.java new file mode 100644 index 00000000..6e833d5d --- /dev/null +++ b/edisyn/gui/CrossfadeEnvelopeDisplay.java @@ -0,0 +1,189 @@ +/*** + Copyright 2017 by Sean Luke + Licensed under the Apache License version 2.0 +*/ + +package edisyn.gui; + +import edisyn.*; +import java.awt.*; +import java.awt.geom.*; +import javax.swing.border.*; +import javax.swing.*; +import java.awt.event.*; + + +public class CrossfadeEnvelopeDisplay extends JComponent implements Updatable + { + Synth synth; + Color color; + Color indexColor; + + double lengthConstants[]; + double heightConstants[]; + double fadeConstants[]; + String lengthKeys[]; + String heightKeys[]; + String fadeKeys[]; + String seqLenKey; + String seqIndexKey; + String magnifyKey; + + int width = 128; + + public void setPreferredWidth(int width) + { + this.width = width; + } + + public int getPreferredWidth() + { + return this.width; + } + + public void update(String key, Model model) + { + repaint(); + } + + public Dimension getPreferredSize() { return new Dimension(width, 84); } + public Dimension getMinimiumSize() { return new Dimension(84, 84); } + public Dimension getMaximumSize() { return new Dimension(100000, 100000); } + + public Color getColor() { return color; } + + public CrossfadeEnvelopeDisplay(Synth synth, Color color, Color indexColor, String seqIndexKey, String seqLenKey, + double[] lengthConstants, double[] heightConstants, double[] fadeConstants, + String[] lengthKeys, String[] heightKeys, String[] fadeKeys, String magnifyKey) + { + super(); + this.synth = synth; + this.color = color; + this.indexColor = indexColor; + this.seqIndexKey = seqIndexKey; + this.seqLenKey = seqLenKey; + this.magnifyKey = magnifyKey; + + this.lengthConstants = lengthConstants; + this.heightConstants = heightConstants; + this.fadeConstants = fadeConstants; + this.lengthKeys = lengthKeys; + this.heightKeys = heightKeys; + this.fadeKeys = fadeKeys; + + int len = lengthConstants.length; + if (len < 1) + throw new IllegalArgumentException("Length must be >= 1"); + if (heightConstants.length != lengthConstants.length) + throw new IllegalArgumentException("heightConstants is not normal size"); + if (fadeConstants.length != lengthConstants.length) + throw new IllegalArgumentException("fadeConstants is not normal size"); + if (lengthKeys.length != lengthConstants.length) + throw new IllegalArgumentException("lengthKeys is not normal size"); + if (heightKeys.length != lengthConstants.length) + throw new IllegalArgumentException("heightKeys is not normal size"); + if (fadeKeys.length != lengthConstants.length) + throw new IllegalArgumentException("fadeKeys is not normal size"); + + Model model = synth.getModel(); + + for(int i = 0; i < lengthKeys.length; i++) + if (lengthKeys[i] != null) + model.register(lengthKeys[i], this); + + for(int i = 0; i < heightKeys.length; i++) + if (heightKeys[i] != null) + model.register(heightKeys[i], this); + + for(int i = 0; i < fadeKeys.length; i++) + if (fadeKeys[i] != null) + model.register(fadeKeys[i], this); + + model.register(seqIndexKey, this); + model.register(seqLenKey, this); + if (magnifyKey != null) + model.register(magnifyKey, this); + + setBackground(Style.BACKGROUND_COLOR()); + } + + public void paintComponent(Graphics g) + { + Graphics2D graphics = (Graphics2D) g; + + Rectangle rect = getBounds(); + rect.x = 0; + rect.y = 0; + + graphics.setPaint(Style.BACKGROUND_COLOR()); + graphics.fill(rect); + + Model model = synth.getModel(); + + int magnify = 1; + if (magnifyKey != null) + magnify = model.get(magnifyKey, 1); + + rect.width -= Style.ENVELOPE_DISPLAY_BORDER_THICKNESS() * 2; + rect.height -= Style.ENVELOPE_DISPLAY_BORDER_THICKNESS() * 2; + + rect.x += Style.ENVELOPE_DISPLAY_BORDER_THICKNESS(); + rect.y += Style.ENVELOPE_DISPLAY_BORDER_THICKNESS(); + + int seqLen = model.get(seqLenKey, 0); + int seqIndex = model.get(seqIndexKey, 0) - model.getMin(seqIndexKey); + int start = seqIndex; + /* if (start > 0) + start --; */ + if (start < 0) + start = 0; + else if (start > 0) + start --; + + double x = rect.x; + double y = rect.y + rect.height; + for(int i = start; i < seqLen && x <= rect.width - (rect.width / 5); i++) + { + double fadein = 0.0; + if (i != 0) + fadein = model.get(fadeKeys[i - 1], 0) * fadeConstants[i - 1] * magnify; + double fadeout = model.get(fadeKeys[i], 0) * fadeConstants[i] * magnify; + double height = model.get(heightKeys[i], 0) * heightConstants[i]; + double length = model.get(lengthKeys[i], 0) * lengthConstants[i] * magnify; + + if (i == seqIndex) + graphics.setColor(indexColor); + else + graphics.setColor(color); + + graphics.fill(new Ellipse2D.Double(x - Style.ENVELOPE_DISPLAY_MARKER_WIDTH()/2.0, + y - Style.ENVELOPE_DISPLAY_MARKER_WIDTH()/2.0, + Style.ENVELOPE_DISPLAY_MARKER_WIDTH(), Style.ENVELOPE_DISPLAY_MARKER_WIDTH())); + + graphics.draw(new Line2D.Double(x, y, + x + rect.width * fadein, y - (rect.height * height))); + + graphics.fill(new Ellipse2D.Double(x + rect.width * fadein - Style.ENVELOPE_DISPLAY_MARKER_WIDTH()/2.0, + y - (rect.height * height) - Style.ENVELOPE_DISPLAY_MARKER_WIDTH()/2.0, + Style.ENVELOPE_DISPLAY_MARKER_WIDTH(), Style.ENVELOPE_DISPLAY_MARKER_WIDTH())); + + graphics.draw(new Line2D.Double(x + rect.width * fadein, y - (rect.height * height), + x + rect.width * (fadein + length), y - (rect.height * height))); + + graphics.fill(new Ellipse2D.Double(x + rect.width * (fadein + length) - Style.ENVELOPE_DISPLAY_MARKER_WIDTH()/2.0, + y - (rect.height * height) - Style.ENVELOPE_DISPLAY_MARKER_WIDTH()/2.0, + Style.ENVELOPE_DISPLAY_MARKER_WIDTH(), Style.ENVELOPE_DISPLAY_MARKER_WIDTH())); + + graphics.draw(new Line2D.Double(x + rect.width * (fadein + length), y - (rect.height * height), + x + rect.width * (fadein + length + fadeout), y)); + + graphics.fill(new Ellipse2D.Double(x + rect.width * (fadein + length + fadeout) - Style.ENVELOPE_DISPLAY_MARKER_WIDTH()/2.0, + y - Style.ENVELOPE_DISPLAY_MARKER_WIDTH()/2.0, + Style.ENVELOPE_DISPLAY_MARKER_WIDTH(), Style.ENVELOPE_DISPLAY_MARKER_WIDTH())); + + x += rect.width * (fadein + length); + } + } + } + + diff --git a/edisyn/gui/Dial.java b/edisyn/gui/Dial.java deleted file mode 100644 index 8043dc9a..00000000 --- a/edisyn/gui/Dial.java +++ /dev/null @@ -1,317 +0,0 @@ -/*** - Copyright 2017 by Sean Luke - Licensed under the Apache License version 2.0 -*/ - -package edisyn.gui; - -import edisyn.*; -import java.awt.*; -import java.awt.geom.*; -import javax.swing.border.*; -import javax.swing.*; -import java.awt.event.*; - - -/** - A dial which the user can modify with the mouse. - The dial updates the model and changes in response to it. - The dial has no label: for a labelled dial, see LabelledDial. - - @author Sean Luke -*/ - - - -public class Dial extends NumericalComponent - { - // What's going on? Is the user changing the dial? - public static final int STATUS_STATIC = 0; - public static final int STATUS_DIAL_DYNAMIC = 1; - int status = STATUS_STATIC; - Color staticColor; - - // The largest range that the dial can represent. 127 is reasonable - // for most synths but some synths (DSI ahem) will require more. - public static final int MAX_EXTENT = 127; - - // Used to convert the number into text shown in the dial - Map map; - - // The state when the mouse was pressed - int startState; - // The mouse position when the mouse was pressed - int startX; - int startY; - - // Is the mouse pressed? This is part of a mechanism for dealing with - // a stupidity in Java: if you PRESS in a widget, it'll be told. But if - // you then drag elsewhere and RELEASE, the widget is never told. - boolean mouseDown; - - // how much should be subtracted from the value in the model before - // it is displayed onscreen? - int subtractForDisplay = 0; - - // Field in the center of the dial - JLabel field = new JLabel("88888", SwingConstants.CENTER); - - - - public void update(String key, Model model) { field.setText(map(getState())); repaint(); } - - /** Maps the stored number to a text string. - Override this as you see fit. By default it calls its Map object (typically provided by the LabelledDial). - Otherwise it subtracts - the requested amount from the value and converts to a string directly. */ - public String map(int val) { if (map != null) return map.map(val); else return "" + (val - subtractForDisplay); } - - public Dimension getPreferredSize() { return new Dimension(55, 55); } - public Dimension getMinimumSize() { return new Dimension(55, 55); } - - /** Returns the current Map object, which maps numbers to strings. */ - public Map getMap() { return map; } - /** Sets the current Map object, which maps numbers to strings. */ - public void setMap(Map v) - { - map = v; - if (map != null) - { - field.setText(map(getState())); - } - } - - void mouseReleased(MouseEvent e) - { - if (mouseDown) - { - status = STATUS_STATIC; - repaint(); - mouseDown = false; - } - } - - /** Makes a dial for the given key parameter on the given synth, and with the given color and - minimum and maximum. If there is no map, then prior to display, subtractForDisplay is - SUBTRACTED from the parameter value. You can use this to convert 0...127 in the model - to -64...63 on-screen, for example. */ - public Dial(Synth synth, String key, Color staticColor, int min, int max, int subtractForDisplay) - { - this(synth, key, staticColor); - setMin(min); - setMax(max); - } - - public Dial(Synth synth, String key, Color staticColor) - { - super(synth, key); - - this.staticColor = staticColor; - - field.setFont(Style.DIAL_FONT); - field.setBackground(Style.TRANSPARENT); - field.setForeground(Style.TEXT_COLOR); - - addMouseWheelListener(new MouseWheelListener() - { - public void mouseWheelMoved(MouseWheelEvent e) - { - int val = getState() - e.getWheelRotation(); - if (val > getMax()) val = getMax(); - if (val < getMin()) val = getMin(); - setState(val); - } - }); - - addMouseListener(new MouseAdapter() - { - public void mousePressed(MouseEvent e) - { - mouseDown = true; - startX = e.getX(); - startY = e.getY(); - startState = getState(); - status = STATUS_DIAL_DYNAMIC; - repaint(); - } - - public void mouseReleased(MouseEvent e) - { - status = STATUS_STATIC; - repaint(); - } - }); - - addMouseMotionListener(new MouseMotionAdapter() - { - public void mouseDragged(MouseEvent e) - { - //int x = e.getX() - startX; - int y = -(e.getY() - startY); - int range = (getMax() - getMin() + 1 ); - int multiplicand = 1; - if (range < MAX_EXTENT) - multiplicand = MAX_EXTENT / range; - - // at present we're just going to use y. It's confusing to use either y or x. - setState(startState + y / multiplicand); - field.setText(map(getState())); - repaint(); - } - }); - - // This gunk fixes a BAD MISFEATURE in Java: mouseReleased isn't sent to the - // same component that received mouseClicked. What the ... ? Asinine. - // So we create a global event listener which checks for mouseReleased and - // calls our own private function. EVERYONE is going to do this. - long eventMask = AWTEvent.MOUSE_EVENT_MASK; - - Toolkit.getDefaultToolkit().addAWTEventListener( new AWTEventListener() - { - public void eventDispatched(AWTEvent e) - { - if (e instanceof MouseEvent && e.getID() == MouseEvent.MOUSE_RELEASED) - { - mouseReleased((MouseEvent)e); - } - } - }, eventMask); - - setState(getState()); - setLayout(new BorderLayout()); - add(field, BorderLayout.CENTER); - field.setText(map(getState())); - repaint(); - } - - /** Returns the actual square within which the Dial's circle - is drawn. */ - public Rectangle getDrawSquare() - { - Insets insets = getInsets(); - Dimension size = getSize(); - int width = size.width - insets.left - insets.right; - int height = size.height - insets.top - insets.bottom; - - // How big do we draw our circle? - if (width > height) - { - // base it on height - int h = height; - int w = h; - int y = insets.top; - int x = insets.left + (width - w) / 2; - return new Rectangle(x, y, w, h); - } - else - { - // base it on width - int w = width; - int h = w; - int x = insets.left; - int y = insets.top + (height - h) / 2; - return new Rectangle(x, y, w, h); - } - } - - public boolean isSymmetric() { if (map != null) return map.isSymmetric(); else return getCanonicalSymmetric(); } - - public boolean getCanonicalSymmetric() { return subtractForDisplay == 64; } - - public double getCanonicalStartAngle() - { - if (isSymmetric()) - { - return 90 + (270 / 2); - } - else - { - return 270; - } - } - - public double getStartAngle() - { - if (map != null) - return map.getStartAngle(); - else return getCanonicalStartAngle(); - } - - - public void paintComponent(Graphics g) - { - int min = getMin(); - int max = getMax(); - - Synth.prepareGraphics(g); - - Graphics2D graphics = (Graphics2D) g; - - Rectangle rect = getBounds(); - rect.x = 0; - rect.y = 0; - graphics.setPaint(Style.BACKGROUND_COLOR); - graphics.fill(rect); - rect = getDrawSquare(); - graphics.setPaint(Style.DIAL_UNSET_COLOR); - graphics.setStroke(Style.DIAL_THIN_STROKE); - Arc2D.Double arc = new Arc2D.Double(); - - double startAngle = getStartAngle(); - double interval = -270; - - arc.setArc(rect.getX() + Style.DIAL_STROKE_WIDTH / 2, rect.getY() + Style.DIAL_STROKE_WIDTH/2, rect.getWidth() - Style.DIAL_STROKE_WIDTH, rect.getHeight() - Style.DIAL_STROKE_WIDTH, startAngle, interval, Arc2D.OPEN); - - graphics.draw(arc); - graphics.setStroke(Style.DIAL_THICK_STROKE); - arc = new Arc2D.Double(); - - int state = getState(); - interval = -((state - min) / (double)(max - min) * 265) - 5; - - if (status == STATUS_DIAL_DYNAMIC) - { - graphics.setPaint(Style.DIAL_DYNAMIC_COLOR); - if (state == min) - { - interval = -5; - // If we're basically at zero, we still want to show a little bit while the user is scrolling so - // he gets some feedback. - //arc.setArc(rect.getX() + Style.DIAL_STROKE_WIDTH / 2, rect.getY() + Style.DIAL_STROKE_WIDTH/2, rect.getWidth() - Style.DIAL_STROKE_WIDTH, rect.getHeight() - Style.DIAL_STROKE_WIDTH, 270, -5, Arc2D.OPEN); - } - else - { - //arc.setArc(rect.getX() + Style.DIAL_STROKE_WIDTH / 2, rect.getY() + Style.DIAL_STROKE_WIDTH/2, rect.getWidth() - Style.DIAL_STROKE_WIDTH, rect.getHeight() - Style.DIAL_STROKE_WIDTH, 270, -((state - min) / (double)(max - min) * 265) - 5, Arc2D.OPEN); - } - } - else - { - graphics.setPaint(staticColor); - if (state == min) - { - interval = 0; - // do nothing. Here we'll literally draw a zero - } - else - { - //arc.setArc(rect.getX() + Style.DIAL_STROKE_WIDTH / 2, rect.getY() + Style.DIAL_STROKE_WIDTH/2, rect.getWidth() - Style.DIAL_STROKE_WIDTH, rect.getHeight() - Style.DIAL_STROKE_WIDTH, 270, -((state - min) / (double)(max - min) * 265) - 5, Arc2D.OPEN); - } - } - - arc.setArc(rect.getX() + Style.DIAL_STROKE_WIDTH / 2, rect.getY() + Style.DIAL_STROKE_WIDTH/2, rect.getWidth() - Style.DIAL_STROKE_WIDTH, rect.getHeight() - Style.DIAL_STROKE_WIDTH, startAngle, interval, Arc2D.OPEN); - graphics.draw(arc); - } - - /** Interface which converts integers into appropriate string values to display in the center of the dial. */ - public interface Map - { - /** Maps an integer to an appropriate String value. */ - public String map(int val); - public boolean isSymmetric(); - public double getStartAngle(); - } - } - - - - diff --git a/edisyn/gui/EnvelopeDisplay.java b/edisyn/gui/EnvelopeDisplay.java index 13595c53..7330e78f 100644 --- a/edisyn/gui/EnvelopeDisplay.java +++ b/edisyn/gui/EnvelopeDisplay.java @@ -11,41 +11,122 @@ import javax.swing.border.*; import javax.swing.*; import java.awt.event.*; +import java.util.*; /** - Abstract superclass of widgets which maintain numerical values in the model. - Each such widget maintains a KEY which is the parameter name in the model. - Widgets can share the same KEY and thus must update to reflect changes by - the other widget. + A tool to display envelopes. You will provide one of two collections of items: -

You will notably have to implement the update(...) method to - revise the widget in response to changes in the model. +