WebMidi is a technology that has been emerging for a few years now, and it's finally supported in Chrome by default. Even browsers that don't support it can use the WebMIDIAPIShim by Chris Wilson to add support. For a list of current support, check out caniuse.com.
If you are not a musician or audio hobbyist, you might not have any midi devices setup, and will not be able to get the full experience on websites that are starting to enable midi features.
Fear not, you can setup virtual midi devices that can be used on these websites. There are ways to do this on all operating systems. This tutorial is focused on OSX, but Window users can check out tools like Midi Yoke, LoopBe1, Sony Virtual MIDI Router, or loopMIDI and Linux users can try ALSA or QJackCtl.
You can open Audio MIDI Setup by searching Spotlight, by looking in /Applications/Utilities/, or by using Launchpad:
Once Audio MIDI Setup is open, you can open the Midi Studio window:
Now you can open IAC Driver (Inter Application Communications Driver) by double clicking on the icon:
Which opens this window:
After making the following changes you should see:
Now that you have your virtual driver / port setup, you can install / use a virtual MIDI keyboard on your favorite WebMidi enabled website. For instructions on installing a Virtual MIDI Keyboard, continue reading below.
A few options:
Since I use homebrew with cask, I'm going to install midikeys. You can do this by opening your command line, and running:
brew cask install midikeys
Now that MidiKeys is installed, you can open it via Launchpad:
Once the program is opened, make sure to choose "IAC Drive: WebMidi" from the "Destination" dropdown:
You can now click on keys from that virtual keyboard to send MIDI events to the WebMidi virtual port you created earlier.
There are other options for virtual keyboards / software, but this is a quick way to get up and running. More advanced users can try out programs like Pure Data by running
brew cask install pd
and creating their own virtual midi keyboard / instrument for usage with WebMidi.
Better yet, create your own WebMidi instrument that sends output to other WebMidi enabled devices!
]]>With newer browsers, you are finally able to implement some of the things you could do in flash years ago, without requiring browser plugins. I created Audio Grid as an experiment to explore this fact.
It's using timbre.js for it's core functionality, but is also making use of a few libraries that I recently created for use in Audio Sort:
soundfont2mp3 - a command line utility that lets you extract single note mp3s from soundfont files.
timbre.soundfont.js - play soundfont urls using timbre.js
timbre.mp3_decode.js - a timbre.js plugin that decodes mp3s using jsmad
free-midi - a collection of mp3s generated by soundfont2mp3 and the S. Christian Collins GeneralUser GS Soundfont.
You can see a demo of Audio Grid by clicking on the image below:
]]>Since I've been playing with timbre.js lately, I started to wonder if I could make a web app to accomplish something similar (using javascript and modern web libraries).
I also wanted to attempt to make the audio "sound good".
I came up with something fairly quickly, but then started researching websites that provided similar functionality. The more I researched, the more features I wanted to add. Check the long list of links I researched while working on Audio Sort.
I plan to add more features depending on how bored I get. Check out Audio Sort below:
]]>They wrote a blog post describing the problems CSP headers try to solve, how to prepare your app for including the headers, and listed some of the current limitations you may encounter when using the headers.
I've ran into some of these limitations with the Github userscripts I've authored.
First off, to make a cross browser userscript, there are a few problems that I've ran into. If your userscript needs to access variables on a page, you can use a few techniques:
Of these three, Content Script Injection is the preferred method. It works for both Firefox and Chrome. I've been using a variation of that method for most of the scripts I've written in the past.
Here's how I would typically write my scripts:
// Create some js that you want to inject into the page
var main = function () {};
// Inject our main script into the page
var script = document.createElement('script');
script.textContent = '(' + main.toString() + ')();';
document.body.appendChild(script);
After Github started sending CSP headers, my userscripts broke by throwing errors like:
CSP ERROR: Couldn't parse invalid source 'unsafe-inline'
To fix Firefox, I temporarily removed the Content Script Injection hacks I was using. Here's an example commit I made back in April:
What I forgot when making those commits, is that Chrome userscripts do not work the same way as Firefox userscripts.
To make Chrome userscripts work, I needed to use Content Script Injection. The issue is that Github is sending the following CSP headers:
x-content-security-policy: default-src *; script-src https://github.com https://a248.e.akamai.net https://jobs.github.com https://ssl.google-analytics.com https://secure.gaug.es https://collector.githubapp.com https://gist.github.com; style-src https://github.com https://a248.e.akamai.net 'unsafe-inline'; object-src https://github.com https://a248.e.akamai.net
If you notice, I can only use the Content Script Injection method from the following domains:
I had been hotlinking my scripts from the raw.github.com domain.
To fix, I started hosting my scripts in a Gist.
Since Firefox supports the @require userscript annotation, I rely on that. For Chrome, I am using Content Script Injection. The reason I didn't use Content Script Injection for Firefox, is because @require does some caching, and I was also experiencing some weird behavior in Firefox in which certain requests to gist.github.com were returning no content (0 byte requests)- and my scripts weren't working.
I haven't experienced that behavior with Chrome requests.
Another method I was toying with, is by injecting the scripts via an iframe. Go to any github.com page, and run the following code in Chrome's console:
var injectViaScript = function (fn) {
var script = document.createElement('script');
script.textContent = '(' + fn.toString() + '());';
document.body.appendChild(script);
document.body.removeChild(script);
};
var injectViaIframe = function (fn) {
var fnName = 'dynamic_fn_' + new Date().getTime(),
iframe = document.createElement('iframe');
iframe.onload = function () {
parent.window[fnName] = new Function('(' + fn.toString() + '());');
parent.window[fnName]();
parent.document.body.removeChild(iframe);
};
document.body.appendChild(iframe);
};
// This will throw an error
injectViaScript(function () {
alert('Hello from script!');
});
// This will work
injectViaIframe(function () {
alert('Hello from iframe!');
});
That seemed like an approach I could take, but was running into scoping issues, so gave up trying. I ended up settling on the @require method for Firefox, and the Content Script Injection from gist.github.com for Chrome.
]]>With the recent CSP Headers that were added to the site, I had to make changes to all my scripts. Since it is starting to be a pain to manage so many scripts, and it's even a bigger pain for people if they want to install all of them, I've consolidated them all into one script:
Eventually, I might make this a legitimate browser extension, and add functionality for turning features on and off. I'll also probably clean up the code, and decommission the "one off" scripts.
At the time of this writing, here are the scripts that are included:
]]>Download here: http://code.google.com/p/open-electribe-editor/downloads/list
npm update
will not update those packages.
There's not an easy way to compare the versions you specified in your package.json file with the latest remotely published version.
In comes npm-dview to the rescue. Check out the project page for screenshots and other usage instructions.
Or install now by running:
npm install -g npm-dview
]]>Download here: http://code.google.com/p/open-electribe-editor/downloads/list
[BUG: #11] Imports/Exports showing up twice in context menu
[BUG: #12] Auto scroll when using context menu.
[BUG: #21] Can't Save and problems to import Patterns
[BUG: #22] Stereo samples not saving correctly. Mono samples not playing correctly.
[FEATURE REQUEST: #18] Add functions for selecting unused items.
[FEATURE REQUEST: #23] Make IsLoop? more clear for stereo samples.
removing unused command: "Export Selected Audio Files"
AudioPlayer no longer hangs, or becomes unresponsive.
The new function is called insertArray().
It allows you to insert an array of objects into a collection using a shell version of Syntactic Sugar.
All it does is loop through the array that was passed in, calling DBCollection.insert() on each object in the array.
If an item in the array is not an object, it will skip the insert, print a warning, and continue processing the next item in the array.
If you don't pass in a valid array, it will throw an error.
Example uses are:
// insert 2 items into myCollection
var myArray = [
{ _id: 1, test: 1 },
{ _id: 2, test: 'foo' },
];
db.myCollection.insertArray(myArray);
// transfer 10 items from collection1 into collection2
db.collection2.insertArray(db.collection1.find().limit(10).toArray());
Below is the function definition. Rather than using polyfills, it relies underscore.js (which is included in mesh.js).
/*jslint nomen: true, plusplus: true */
/*global _, DBCollection, print */
/**
* @function
* @name insertArray
* @memberOf DBCollection
* @param {array} arr - The array of objects to insert.
* @param {object} options - pass through to DBCollection.prototype.insert()
* @param {boolean} _allow_dot - pass through to DBCollection.prototype.insert()
* @throws {Exception} - when arr is not an Array.
*/
DBCollection.prototype.insertArray = function (arr, options, _allow_dot) {
'use strict';
var i, obj;
if (_.isArray(arr)) {
for (i = 0; i < arr.length; i++) {
obj = arr[i];
if (_.isObject(obj) && !_.isFunction(obj)) {
this.insert(obj, options, _allow_dot);
} else {
print('Cannot insert a non-object, so skipping: ' + obj);
}
}
} else {
throw 'first argument is not an array!';
}
};
]]>To get around this issue, I use a simple bookmarklet that takes me back to the document referrer. I place the bookmarklet underneath my back button, so I have easy access to it.
Here's a screenshot of a disabled back button (in a new tab):
Here's a screenshot of my browser with the "referrer" bookmarklet:
To install the bookmarklet, drag this link to your bookmark toolbar:
All the bookmarklet does is:
document.location = document.referrer;
]]>Let's make GitHub better, together.
I saw the section on Revamping Inline Editing, and noticed one of the comments was about the way Github uses the Ace Editor.
Since I've been working with Ace some recently, I decided to work on a userscript that let's you switch themes on Github.
You can check it out here:
Here is the current description from README.md:
Add a "theme" dropdown when editing files in the Github UI.
One night after work, I decided to write a simple web app to do the list comparisons for me.
As of now, the app has the following features:
other operations: sort, reverse, unique, trim
uses the Ace Editor (instead of default textarea elements)
uses Twitter Bootstrap
responsive design: 4 different display sizes will change the layout
live editing: the results/counts will be updated as you add new items to lists
a logo visualization of the currently selected comparison operation
using Web Workers (when available) to support larger datasets
You can check out the project page here:
You can bookmark, and use the application here:
]]>I was going to create a quick webpage to do it for me, but that would have still required me to do a lot of manual copy-n-pasting (copying the html into a form, clicking a button, and pasting the result into a new file).
What I really wanted was a one liner that I could use in my terminal.
I ended up using a combination of cat and sed to get my desired output. The combined result is a shell script called: tojs.
You can install it by running the following commands:
NOTE:
THE FOLLOWING INSTALLATION INSTRUCTIONS ARE OUTDATED. SEE THE UPDATED SECTION BELOW.
sudo curl https://raw.github.com/skratchdot/tojs/1.1.0/tojs.sh -o /usr/local/bin/tojs
sudo chmod +x /usr/local/bin/tojs
UPDATED (12/27/2012):
tojs was converted to a node.js project on December 27th, 2012. Due to this fact, the installation instructions have changed. This project now requires npm and can be installed by running:
npm install -g tojs
]]>This is necessary for users with a large number of repos so that page load times are acceptable (also, there's no need to show all repo info for someone with 1000+ repos).
To find out more about older repositories, you have to visit each repo page.
To help out with this manual process, I've created: Github: Get Missing Descriptions.
This script adds a button that will update "simple" repo listings with the project's description, as well as the last time it was updated. Each button click will make a max of 50 ajax requests. If there are more than 50 repos without descriptions, you will have to click the button multiple times.
You can check it out here:
Here is the current description from README.md:
If there are missing descriptions on a Github profile page, a button will be added. When clicked, ajax requests will be made to grab the descriptions.
Before clicking the button:
After clicking the button:
The learn more link took me to this page:
http://www.chromium.org/administrators/policy-list-3#ExtensionInstallSources
Following the directions for OSX, I tried adding the ExtensionInstallSources preference via the command line:
Chrome:
defaults write com.google.Chrome ExtensionInstallSources -array "https://github.com/skratchdot/*"
Chrome Canary:
defaults write com.google.Chrome.canary ExtensionInstallSources -array "https://github.com/skratchdot/*"
I restarted Chrome, and still could not install my userscript.
After reading through this bug:
http://code.google.com/p/chromium/issues/detail?id=138054
I found the --enable-easy-off-store-extension-install switch. By using that, I was able to start Chrome from the command line to install my userscript.
Chrome:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --enable-easy-off-store-extension-install
Chrome Canary:
/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --enable-easy-off-store-extension-install
It would be nice if the "Tools"->"Extensions" page (aka chrome://chrome/extensions/) had a list of whitelist/blacklist URLs that you could edit. That page allows you to enter "Developer Mode", so it makes sense to have a few more options there.
EDIT:
This workaround exists as well:
Since I work in the shell some, and have written a few shell extensions, I decided to create mesh: MongoDB Extended Shell.
The hope is that it will grow into a collection of community created scripts that add functionality to the default mongo shell (while not interfering with it).
So far, I've included a few utility libraries: underscore.js, underscore.string.js, and moment.js.
I've also included some of the extensions that I've created in the past. Additionally, there's some code so that "console" calls end up wrapping the built in print() function. The idea is that eventually I'll add some logic to the console calls so they behave in a more "standard" way.
Another feature is the mesh.setPrompt() function. You can pass in a function, or a number 0-4. There are a few different types of prompts you can use:
mesh.setPrompt() was inspired by the blog post: http://www.kchodorow.com/blog/2011/06/27/ps1/
Check it out here (and download) here: https://github.com/skratchdot/mesh/
]]>Below is the function I came up with:
<cfscript>
private struct function convertStringToStruct(required string key, required any value, string delimiter = ".") {
var obj = StructNew();
var first = ListFirst(arguments.key, arguments.delimiter);
var rest = ListRest(arguments.key, arguments.delimiter);
if (Len(rest)) {
obj[first] = convertStringToStruct(rest, arguments.value, arguments.delimiter);
} else {
obj[first] = arguments.value;
}
return obj;
}
</cfscript>
Here's a quick example usage:
<cfscript>
// Declare a struct. We will later append to this
obj = StructNew();
// Create a few top level keys
obj["ab"] = "foo";
obj["b"] = "bar";
// Append a dynamically created structure
StructAppend(obj, convertStringToStruct("a.b.c", "baz"), false);
// Show our output
WriteDump(obj);
</cfscript>
The output from the above example:
Here is a gist containing the code:
]]>Recently I've found the need to take screenshots of portions of my screen, and add a border to the resultant images. Most of the methods I was using were very time consuming.
I decided to script something out so I could easily use the commandline to accomplish this task.
After some evaluation, I decided to use screencapture and ImageMagick.
ImageMagick comes with of few utilities, one of which is import.
I could have used that tool exclusively (and not used screencapture) if it weren't for the fact that import relies on the X Window System (in OSX it's called X11). I wanted something that could take a screenshot without being in an X11 window.
In the end, I created a shell script called framecapture.
It's a very basic script:
#!/bin/bash
screencapture -is "$1"
convert "$1" -mattecolor blue -frame 6x6+2+2 "$1"
But I decided to add some error handling, and optional parameters to it.
You can view it on Github, or install it by running:
sudo curl https://raw.github.com/skratchdot/framecapture/master/framecapture.sh -o /usr/local/bin/framecapture
sudo chmod +x /usr/local/bin/framecapture
]]>To save on some of that manual activity, I created a new userscript today: Github: Twitter Link.
You can check it out here:
Here is the current description from README.md:
This is a user script that adds a twitter link on Github profile pages if a corresponding user name exists at Twitter. It utilizes local storage to minimize multiple Twitter API requests. If a corresponding user name is not found, then we will not check for it again. At some point I may add a "time limit" on when to check again.
Twitter account exists:
Twitter account doesn't exist:
Download here: http://code.google.com/p/open-electribe-editor/downloads/list
]]>There are a few ways to get around this. One way is to use a console polyfill. Another way is to use a console wrapper. Yet one additional way is by creating empty functions for all the available window.console calls.
This blog post provides some code to achieve the third method listed above. The code first ensures that window.console exists. It then ensures that all functions in the Firebug Console API exist in the window.console object. We default to an empty function so older browsers don't throw errors. If a given window.console function already exists, we will use the default browser behavior.
/*jslint browser: true, plusplus: true */
(function (window) {
'use strict';
var i = 0,
emptyFunction = function () {},
functionNames = [
'assert', 'clear', 'count', 'debug', 'dir',
'dirxml', 'error', 'exception', 'group', 'groupCollapsed',
'groupEnd', 'info', 'log', 'profile', 'profileEnd', 'table',
'time', 'timeEnd', 'timeStamp', 'trace', 'warn'
];
// Make sure window.console exists
window.console = window.console || {};
// Make sure all functions exist
for (i = 0; i < functionNames.length; i++) {
window.console[functionNames[i]] = window.console[functionNames[i]] || emptyFunction;
}
}(window));
/* https://www.skratchdot.com/2012/05/prevent-console-calls-from-throwing-errors/ */
(function(b){var a=0,c=function(){},d=["assert","clear","count","debug","dir","dirxml","error","exception","group","groupCollapsed","groupEnd","info","log","profile","profileEnd","table","time","timeEnd","timeStamp","trace","warn"];b.console=b.console||{};for(a=0;a<d.length;a++){b.console[d[a]]=b.console[d[a]]||c}}(window));
]]>I always forget what steps to take to "remember my last OS" when booting.
First, you need to modify /etc/default/grub:
sudo vi /etc/default/grub
By adding the following 2 lines:
GRUB_DEFAULT=saved
GRUB_SAVEDEFAULT=true
After modifying this file, you need to update GRUB's configuration by executing the following 2 commands:
sudo grub-mkconfig
sudo update-grub
See the Ubuntu Grub2 Help Page or this Stack Overflow Answer
]]>I saw a post on HackerNews in which someone created a pure CSS version of the google doodle.
I created a JSFiddle snippet that day, but never posted it anywhere.
Anyways, I've cleaned the example up a bit, and posted it below. It now contains pure CSS3 controls to turn the demo on/off or change the speed of the animation.
<style type="text/css">
img#zoopraxiscope {
width:230px;
height:230px;
}
@-webkit-keyframes spin {
from { -webkit-transform: rotate(0deg); }
to { -webkit-transform: rotate(-360deg); }
}
@-moz-keyframes spin {
from { -moz-transform: rotate(0deg); }
to { -moz-transform: rotate(-360deg); }
}
@-ms-keyframes spin {
from { -ms-transform: rotate(0deg); }
to { -ms-transform: rotate(-360deg); }
}
a#animation-on:target~input[value=fast]:checked~img#zoopraxiscope {
-webkit-animation: spin 230ms infinite linear;
-moz-animation: spin 230ms infinite linear;
-ms-animation: spin 230ms infinite linear;
}
a#animation-on:target~input[value=medium]:checked~img#zoopraxiscope {
-webkit-animation: spin 920ms infinite linear;
-moz-animation: spin 920ms infinite linear;
-ms-animation: spin 920ms infinite linear;
}
a#animation-on:target~input[value=slow]:checked~img#zoopraxiscope {
-webkit-animation: spin 2000ms infinite linear;
-moz-animation: spin 2000ms infinite linear;
-ms-animation: spin 2000ms infinite linear;
}
</style>
<form style="text-align:center">
<h3>Turn Animation</h3>
<a id="animation-off" href="#animation-off">Off</a>
<a id="animation-on" href="#animation-on">On</a>
<br />
<h3>Speed:</h3>
<label for="fast">Fast:</label>
<input type="radio" name="speed" id="fast" value="fast" checked="checked" />
<br />
<label for="medium">Medium:</label>
<input type="radio" name="speed" id="medium" value="medium" />
<br />
<label for="slow">Slow:</label>
<input type="radio" name="speed" id="slow" value="slow" />
<div style="height:50px"> </div>
<img id="zoopraxiscope" src="/images/posts/2012/05/28/zoopraxiscope.jpg" />
</form>
Inspired by the Google Doodle on April 9th, 2012 celebrating Eadweard J. Muybridge
See:
See also:
]]>Anyways, after replacing the RAM, Windows slowed down dramatically. The startup screen was taking over 10 minutes to load. I tried disabling the page file (and virtual memory), then rebooted. Windows was still taking forever to start.
Once it finally loaded, everything was super slow. The CPU wasn't really spiking, and the disk usage didn't look crazy, but any action took forever. Even the mouse cursor was slow and jumpy.
When I would boot into Ubuntu, things were fine. I couldn't figure out what was wrong with Windows.
I tried running Memtest, and the tests were passing.
Finally, someone suggested trying to update the BIOS. That turned out to be the smoking gun. I upgraded the BIOS from A04 to A08, and rebooted Windows. Everything started working at full speed again. I'm not sure if it was the upgrade, or the simply resetting/flashing the BIOS, but things started working.
]]>One issue that took a while for me to figure out, was a small typo.
In _layouts/post.html, I was using the following syntax to include my post content:
{% raw %}{{ page.content }}{% endraw %}
As I was converting my posts to use markdown syntax, the posts weren't "markifying". It turns out, all I needed to do was use this syntax:
{% raw %}{{ content }}{% endraw %}
]]>I tried Variety out, but it didn't quite do what I wanted, and it also created a few DBs/Collections on my server that I didn't expect.
Over the weekend, I decided to write my own implementation called: mongodb-schema. After finishing that script, I created a few more shell extensions.
Please note, these are just quick examples I whipped up, and are not really meant to be run on large collections (unless you use "limit", or are willing to wait a really long time). They are definitely not "production ready".
Here are the scripts:
mongodb-distinct-types - Similar to the db.myCollection.distinct() function, distinctTypes() will return "types" rather than "values".
mongodb-flatten - The flatten() function is a mapReduce that flattens documents into key/value pairs.
mongodb-schema - A schema analysis tool for MongoDB.
mongodb-wild - Adds a wildcard search to the mongodb shell.
[FEATURE REQUEST: #3] Added the ability to import patterns from other .esx files
[TODO: #13] Added "reset perspective" functionality
[TODO: #14] Fixed unclear message when dragging audio files
[BUG: #10] Fixed rounding issue when setting SampleTune
Download here: http://code.google.com/p/open-electribe-editor/downloads/list
]]>You can find the source code here:
https://github.com/skratchdot/domFormat
And some examples here:
http://projects.skratchdot.com/domFormat/examples/index-html5.html
Here are 2 bookmarklets:
]]>The purpose of this, is to deploy a set of files to a different environment. I can just copy the "DEPLOY" folder to any number of servers. If there is a bug that made it through QA, and we need to immediately rollback changes, we can just copy the "RESTORE" folder to all the same servers.
I just wrote a similar script for Git: Git-Diff-Build-Script
While writing that script, I didn't read the Git docs closely enough, and missed the "git diff" --diff-filter parameter. Because of this, I thought I would have to use AWK to sanitize my list of files, but then I read about the --diff-filter parameter, and changed 2 lines of code to 1:
USING AWK:
info="$(git diff origin/prod origin/dev --name-status)"
files="$(echo "$info" | awk '$1 ~/M|A/ {print $2}')"
USING diff-filter:
files="$(git diff origin/prod origin/dev --name-only --diff-filter=MA)"
]]><script type="text/javascript">
var test = #SerializeJSON("test" & chr(8232))#;
</script>
To "fix" the bug, you can replace SerializeJSON() with SafeSerializeJSON() like this:
<script type="text/javascript">
var test = #SafeSerializeJSON("test" & chr(8232))#;
</script>
The SafeSerializeJSON() function looks like this:
<cffunction name="SafeSerializeJSON" output="false" access="private" returntype="string">
<cfargument name="obj" type="any" required="true" />
<cfargument name="serializeQueryByColumns" type="boolean" required="false" default="false" />
<cfset var jsonOutput = SerializeJSON(arguments.obj, arguments.serializeQueryByColumns) />
<cfset jsonOutput = Replace(jsonOutput, chr(8232), "\u2028", "all") />
<cfset jsonOutput = Replace(jsonOutput, chr(8233), "\u2029", "all") />
<cfreturn jsonOutput />
</cffunction>
I've created a gist that explores the issue in slightly more detail:
]]>Pattern Tab improvements:
Can drag-and-drop patterns
Adding "Pattern Editor" tab
Adding "FX" tab
Adding "Parts" tab
Adding "Motion Sequences" tab
Can move patterns around without changing what songs they are used in.
Sample Tab improvements:
Adding "Pattern/Part Usage" tab
Can move samples around without changing what patterns they are used in.
Song Tab improvements:
Fixing Typos
Adding Sample/Pattern labels that show number & name
Adding Tempo validation
Download here: http://code.google.com/p/open-electribe-editor/downloads/list
]]>Splash screen works
Changing the required Java version to J2SE-1.5 (so Older Macs are supported)
Marking a few property labels/values as READ_ONLY
Download here: http://code.google.com/p/open-electribe-editor/downloads/list
]]>Initial Release
Patterns Tab is incomplete
Song Events tab is incomplete
Download here: http://code.google.com/p/open-electribe-editor/downloads/list
]]>PROS:
CONS:
NOTES:
This is my least favorite, because I couldn't come up with a cfquery to test all datasources. It will work for some datasources, but not all.
SOURCE:
<cffunction name="verifyDsnList1" output="true" returntype="void">
<cfargument name="list" type="string" required="true" />
<cfargument name="delimiter" type="string" required="false" default="," />
<cfset var local = StructNew() />
<cfoutput>
<hr />
<b><u>veriftyDsnList1</u></b>
<cfloop list="#arguments.list#" index="local.currentName">
<cftry>
<hr />
<cfquery name="local.qVerifyDatasource" datasource="#local.currentName#">
SELECT 1
</cfquery>
<cfif local.qVerifyDatasource.RecordCount gt 0>
DATASOURCE: #local.currentName# [VERIFIED=true]
<cfelse>
DATASOURCE: #local.currentName# [VERIFIED=false]
</cfif>
<cfcatch>
DATASOURCE: #local.currentName# [VERIFIED=false] [ERROR: #cfcatch.message#]
</cfcatch>
</cftry>
</cfloop>
<hr />
</cfoutput>
</cffunction>
PROS:
CONS:
SOURCE:
<cffunction name="verifyDsnList2" output="true" returntype="void">
<cfargument name="list" type="string" required="true" />
<cfargument name="delimiter" type="string" required="false" default="," />
<cfset var local = StructNew() />
<cfset local.dsService = CreateObject("java", "coldfusion.server.ServiceFactory").DataSourceService />
<cfoutput>
<hr />
<b><u>veriftyDsnList2</u></b>
<cfloop list="#arguments.list#" index="local.currentName">
<cftry>
<hr />
<cfif local.dsService.verifyDatasource(local.currentName)>
DATASOURCE: #local.currentName# [VERIFIED=true]
<cfelse>
DATASOURCE: #local.currentName# [VERIFIED=false]
</cfif>
<cfcatch>
DATASOURCE: #local.currentName# [VERIFIED=false] [ERROR: #cfcatch.message#]
</cfcatch>
</cftry>
</cfloop>
<hr />
</cfoutput>
</cffunction>
PROS:
CONS:
SOURCE:
<cffunction name="verifyDsnList3" output="true" returntype="void">
<cfargument name="cfide_password" type="string" required="true" />
<cfargument name="list" type="string" required="true" />
<cfargument name="delimiter" type="string" required="false" default="," />
<cfset var local = StructNew() />
<cfset local.admin = createObject("component","cfide.adminapi.administrator").login(arguments.cfide_password) />
<cfset local.dsObj = createObject("component","cfide.adminapi.datasource") />
<cfoutput>
<hr />
<b><u>veriftyDsnList3</u></b>
<cfloop list="#arguments.list#" index="local.currentName">
<hr />
DATASOURCE: #local.currentName# [VERIFIED=#local.dsObj.verifyDSN(local.currentName)#]
</cfloop>
<hr />
</cfoutput>
</cffunction>
Here's a helper function that will return a sorted list of all configured datasource names:
<cffunction name="getDatasourceList" output="false" returntype="string">
<cfargument name="sort_type" type="string" required="false" default="textnocase" hint="Optional. See livedocs for ListSort()." />
<cfargument name="sort_order" type="string" required="false" default="asc" hint="Optional. See livedocs for ListSort()." />
<cfargument name="delimiter" type="string" required="false" default="," hint="Optional. See livedocs for ListSort()." />
<cfset var dsService = CreateObject("java", "coldfusion.server.ServiceFactory").DataSourceService />
<cfset var sDatasources = dsService.getDatasources() />
<cfset var dsList = StructKeyList(sDatasources, arguments.delimiter) />
<cfreturn ListSort(dsList, arguments.sort_type, arguments.sort_order, arguments.delimiter) />
</cffunction>
Here's a small script to test all the functions from this post. For this to work correctly, you'll need to set the correct CFIDE password.
<cfset my_cfide_password = "admin" />
<cfset my_datasource_list = getDatasourceList() />
<cfoutput>
#verifyDsnList1(my_datasource_list)#
<br />
#verifyDsnList2(my_datasource_list)#
<br />
#verifyDsnList3(my_cfide_password, my_datasource_list)#
<br />
</cfoutput>
]]>Cannot insert duplicate key row in object 'MyProductView' with unique index 'IXCL_MyProductView'
NOTE: I've changed the names of the views/tables/columns that were actually used, but you should get the idea.
The update statement that threw the error looked like this:
UPDATE Products SET active = 1 WHERE id = 200
MyProductView was basically just a way to join a few of the product related tables (ie: Sizes, Colors, Inventory, etc).
The index on MyProductView that was causing the constraint violation looked something like this:
CREATE UNIQUE CLUSTERED INDEX IXCL_MyProductView ON MyProductView
(
product_id,
color_id,
size_id,
inventory_id,
currency_id
}
Notice the active flag/column is not in the clustered index (so updating that column on the Products table should not change a row in my view that would violate the unique constraint above). Also, the active column was not in any of the join clauses for the MyProductView, so no rows should've been added or deleted from the view after running the update statement.
To confirm the validity of the IXCL_MyProductView constraint, I could run:
SELECT
product_id, color_id, size_id, inventory_id, currency_id, COUNT(1) AS confirmConstraint
FROM
MyProductView
GROUP BY
product_id, color_id, size_id, inventory_id, currency_id
As long as the IXCL_MyProductView constraint was in place, the confirmConstraint column could only have a value of 1.
I could not figure out why running:
UPDATE Products SET active = 1 WHERE id = 200
would cause a problem. If the query would complete successfully, the constraint would still be valid. In fact, I could run the same query in production just fine. Also, the other developers could run the query just fine on their local DBs. I was the only one who was running into this constraint violation (and I had the same data in my database as the other databases had). After bringing this up with our DBA, he had no idea why the constraint violation would be occur. I wondered if I needed to wrap the UPDATE statement in a TRANSACTION COMMIT block, but our DBA said I shouldn't have to.
After thinking about it for a minute, he asked what version of SQL Server 2005 I was using. It turned out I was not running the same service pack level as the other developers (and what was in production).
I was running "SQL Server 2005 - Developer's Edition (9.0 RTM)". I then upgraded to Service Pack 2: "SQL Server 2005 - Developer's Edition (9.0 SP2)", and the issue disappeared. I was able to run my UPDATE statement without error.
]]>When creating this blog's theme, I decided I wanted the links in the side navigation to take up the full width of the navigation. Also, when hovering, I wanted to highlight the whole link/line. This seems like a very simple thing to do with CSS, but by default, a few of the Wordpress widgets do not allow for easy customization of the placement of the post count. This makes it hard to create sidebar entries in which the whole line is a link.
For example, by default, I could only get the Archive Widget to print out links in the following format:
<li><a href="/2009/09/">September 2009</a> (1)</li>
I wanted the links to be in this format (note the (1) is inside the anchor tag):
<li><a href="/2009/09/">September 2009 (1)</a></li>
Below are the steps I took to override the default behavior of the Archive Widget. For this tutorial, I'm going to modify the "classic" theme that is provided with Wordpress 2.8. You can download both the original "classic" theme as well as my modified version "classic-modified" here.
For this tutorial, the first step to take is to copy the "classic" theme. Rename the copy "classic-modified" and place it in your themes folder.
Now we're going to modify the functions.php file by adding lines 17-18 below:
<?php
/**
* @package WordPress
* @subpackage Classic_Theme
*/
automatic_feed_links();
if ( function_exists('register_sidebar') )
register_sidebar(array(
'before_widget' => '<li id="%1$s" class="widget %2$s">',
'after_widget' => '</li>',
'before_title' => '',
'after_title' => '',
));
// Include the script that replaces the default archive widget
require_once('functions-widgets.php');
?>
Now create a blank file called functions-widgets.php and place it in your themes folder. Next, open the file "/wp-includes/default-widgets.php" and copy lines 210-271. Paste these lines in functions-widgets.php so it looks like the code below (Note I've added the opening and closing php tags):
<?php
/**
* Archives widget class
*
* @since 2.8.0
*/
class WP_Widget_Archives extends WP_Widget {
function WP_Widget_Archives() {
$widget_ops = array('classname' => 'widget_archive', 'description' => __( 'A monthly archive of your blog’s posts') );
$this->WP_Widget('archives', __('Archives'), $widget_ops);
}
function widget( $args, $instance ) {
extract($args);
$c = $instance['count'] ? '1' : '0';
$d = $instance['dropdown'] ? '1' : '0';
$title = apply_filters('widget_title', empty($instance['title']) ? __('Archives') : $instance['title']);
echo $before_widget;
if ( $title )
echo $before_title . $title . $after_title;
if ( $d ) {
?>
<select name="archive-dropdown" onchange='document.location.href=this.options[this.selectedIndex].value;'> <option value=""><?php echo esc_attr(__('Select Month')); ?></option> <?php wp_get_archives(apply_filters('widget_archives_dropdown_args', array('type' => 'monthly', 'format' => 'option', 'show_post_count' => $c))); ?> </select>
<?php
} else {
?>
<ul>
<?php wp_get_archives(apply_filters('widget_archives_args', array('type' => 'monthly', 'show_post_count' => $c))); ?>
</ul>
<?php
}
echo $after_widget;
}
function update( $new_instance, $old_instance ) {
$instance = $old_instance;
$new_instance = wp_parse_args( (array) $new_instance, array( 'title' => '', 'count' => 0, 'dropdown' => '') );
$instance['title'] = strip_tags($new_instance['title']);
$instance['count'] = $new_instance['count'] ? 1 : 0;
$instance['dropdown'] = $new_instance['dropdown'] ? 1 : 0;
return $instance;
}
function form( $instance ) {
$instance = wp_parse_args( (array) $instance, array( 'title' => '', 'count' => 0, 'dropdown' => '') );
$title = strip_tags($instance['title']);
$count = $instance['count'] ? 'checked="checked"' : '';
$dropdown = $instance['dropdown'] ? 'checked="checked"' : '';
?>
<p><label for="<?php echo $this->get_field_id('title'); ?>"><?php _e('Title:'); ?></label> <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo esc_attr($title); ?>" /></p>
<p>
<input class="checkbox" type="checkbox" <?php echo $count; ?> id="<?php echo $this->get_field_id('count'); ?>" name="<?php echo $this->get_field_name('count'); ?>" /> <label for="<?php echo $this->get_field_id('count'); ?>"><?php _e('Show post counts'); ?></label>
<br />
<input class="checkbox" type="checkbox" <?php echo $dropdown; ?> id="<?php echo $this->get_field_id('dropdown'); ?>" name="<?php echo $this->get_field_name('dropdown'); ?>" /> <label for="<?php echo $this->get_field_id('dropdown'); ?>"><?php _e('Display as a drop down'); ?></label>
</p>
<?php
}
}
?>
Now rename:
class WP_Widget_Archives extends WP_Widget {
to:
class MyTheme_Widget_Archives extends WP_Widget {
We did this because we are effectively creating a new widget, so if we want to use the old "Archives" widget, we can.
Now change the following function from:
function WP_Widget_Archives() {
$widget_ops = array('classname' => 'widget_archive', 'description' => __( 'A monthly archive of your blog’s posts') );
$this->WP_Widget('archives', __('Archives'), $widget_ops);
}
to:
function MyTheme_Widget_Archives() {
$widget_ops = array('classname' => 'widget_archive', 'description' => __( 'A monthly archive of your blog’s posts') );
$this->WP_Widget('mytheme_archives', __('MyTheme Archives'), $widget_ops);
}
Notice that we not only changed the function name, but we changed the parameters to the WP_Widget call. We did this for the same reason explained above (this is a "new" widget, and we want to be able to switch back to the old version).
For this next step, we are going to modify the widget(), form(), and update() calls. These are the functions that display the actual widget in the sidebar, and also display the widget details in the WordPress admin console. For this new widget, I am always going to display the post count, and I'm never going to display the archive results as a dropdown, so I can remove a few of the variables that are in the default archive widget ($c, $count, $d, $dropdown). To achieve this, you can modify functions-widgets.php like so:
Before changes:
function widget( $args, $instance ) {
extract($args);
$c = $instance['count'] ? '1' : '0';
$d = $instance['dropdown'] ? '1' : '0';
$title = apply_filters('widget_title', empty($instance['title']) ? __('Archives') : $instance['title']);
echo $before_widget;
if ( $title )
echo $before_title . $title . $after_title;
if ( $d ) {
?>
<select name="archive-dropdown" onchange='document.location.href=this.options[this.selectedIndex].value;'> <option value=""><?php echo esc_attr(__('Select Month')); ?></option> <?php wp_get_archives(apply_filters('widget_archives_dropdown_args', array('type' => 'monthly', 'format' => 'option', 'show_post_count' => $c))); ?> </select>
<?php
} else {
?>
<ul>
<?php wp_get_archives(apply_filters('widget_archives_args', array('type' => 'monthly', 'show_post_count' => $c))); ?>
</ul>
<?php
}
echo $after_widget;
}
function update( $new_instance, $old_instance ) {
$instance = $old_instance;
$new_instance = wp_parse_args( (array) $new_instance, array( 'title' => '', 'count' => 0, 'dropdown' => '') );
$instance['title'] = strip_tags($new_instance['title']);
$instance['count'] = $new_instance['count'] ? 1 : 0;
$instance['dropdown'] = $new_instance['dropdown'] ? 1 : 0;
return $instance;
}
function form( $instance ) {
$instance = wp_parse_args( (array) $instance, array( 'title' => '', 'count' => 0, 'dropdown' => '') );
$title = strip_tags($instance['title']);
$count = $instance['count'] ? 'checked="checked"' : '';
$dropdown = $instance['dropdown'] ? 'checked="checked"' : '';
?>
<p><label for="<?php echo $this->get_field_id('title'); ?>"><?php _e('Title:'); ?></label> <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo esc_attr($title); ?>" /></p>
<p>
<input class="checkbox" type="checkbox" <?php echo $count; ?> id="<?php echo $this->get_field_id('count'); ?>" name="<?php echo $this->get_field_name('count'); ?>" /> <label for="<?php echo $this->get_field_id('count'); ?>"><?php _e('Show post counts'); ?></label>
<br />
<input class="checkbox" type="checkbox" <?php echo $dropdown; ?> id="<?php echo $this->get_field_id('dropdown'); ?>" name="<?php echo $this->get_field_name('dropdown'); ?>" /> <label for="<?php echo $this->get_field_id('dropdown'); ?>"><?php _e('Display as a drop down'); ?></label>
</p>
<?php
}
}
?>
After changes:
function widget( $args, $instance ) {
extract($args);
$title = apply_filters('widget_title', empty($instance['title']) ? __('Archives') : $instance['title']);
echo $before_widget;
if ( $title )
echo $before_title . $title . $after_title;
?>
<ul>
// We will complete this in the next step
</ul>
<?php
}
echo $after_widget;
}
function update( $new_instance, $old_instance ) {
$instance = $old_instance;
$instance['title'] = strip_tags($new_instance['title']);
return $instance;
}
function form( $instance ) {
//Defaults
$instance = wp_parse_args( (array) $instance, array( 'title' => '') );
$title = esc_attr( $instance['title'] );
?>
<p>
<label for="<?php echo $this->get_field_id('title'); ?>"><?php _e( 'Title:' ); ?></label>
<input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo $title; ?>" />
</p>
<?php
}
}
?>
Now there's one final step. We need to actually display the archives. To do that, we need to modify the widget() function. In the last step, we were left with the following line:
<ul>
// We will complete this in the next step
</ul>
We are now going to complete this block of code. Open up "/wp-includes/general-template.php" and copy lines 799-825. Paste them into line 24 of functions-widgets.php which will give you:
<ul>
if ( 'monthly' == $type ) {
$query = "SELECT YEAR(post_date) AS `year`, MONTH(post_date) AS `month`, count(ID) as posts FROM $wpdb->posts $join $where GROUP BY YEAR(post_date), MONTH(post_date) ORDER BY post_date DESC $limit";
$key = md5($query);
$cache = wp_cache_get( 'wp_get_archives' , 'general');
if ( !isset( $cache[ $key ] ) ) {
$arcresults = $wpdb->get_results($query);
$cache[ $key ] = $arcresults;
wp_cache_add( 'wp_get_archives', $cache, 'general' );
} else {
$arcresults = $cache[ $key ];
}
if ( $arcresults ) {
$afterafter = $after;
foreach ( (array) $arcresults as $arcresult ) {
$url = get_month_link( $arcresult->year, $arcresult->month );
$text = sprintf(__('%1$s %2$d'), $wp_locale->get_month($arcresult->month), $arcresult->year);
if ( $show_post_count )
$after = ' ('.$arcresult->posts.')' . $afterafter;
$output .= get_archives_link($url, $text, $format, $before, $after);
}
}
</ul>
The first thing you'll notice is the if ( 'monthly' == $type ) statement. We can remove this clause, because the "custom archive plugin" will only show archives by month (I wanted to keep it simple- no need for other display types). Also, this code is using a few global variables ($wpdb, $wp_locale) that aren't yet being used properly in our version of the widget() function. We need to modify functions-widgets.php again to have the following line which will declare our global variables for use inside the function:
function widget( $args, $instance ) {
global $wpdb, $wp_locale;
extract($args);
Also, when we copied code the first time, we didn't copy a few of the variables being used ($where and $join). Modify functions-widgets.php like so (adding lines 25-27):
<ul>
//filters
$where = apply_filters('getarchives_where', "WHERE post_type = 'post' AND post_status = 'publish'", $r );
$join = apply_filters('getarchives_join', "", $r);
$query = "SELECT YEAR(post_date) AS `year`, MONTH(post_date) AS `month`, count(ID) as posts FROM $wpdb->posts $join $where GROUP BY YEAR(post_date), MONTH(post_date) ORDER BY post_date DESC $limit";
Another thing about the code we copied: we can ignore the $postcount variable (since we are _always showing post counts). We also want to make sure the post count shows up inside our anchor tag. To accomplish that, we can change:
$url = get_month_link( $arcresult->year, $arcresult->month );
$text = sprintf(__('%1$s %2$d'), $wp_locale->get_month($arcresult->month), $arcresult->year);
if ( $show_post_count )
$after = ' ('.$arcresult->posts.')' . $afterafter;
$output .= get_archives_link($url, $text, $format, $before, $after);
to:
$url = get_month_link( $arcresult->year, $arcresult->month );
$text = sprintf(__('%1$s %2$d'), $wp_locale->get_month($arcresult->month), $arcresult->year);
$text .= ' ('.$arcresult->posts.')' . $afterafter;
$output .= get_archives_link($url, $text, $format, '<li>', '</li>');
Also add the correct output line before the widget() function ends:
echo '<ul>' . $output . '</ul>';
echo $after_widget;
Our hacked/modified version is almost complete. The last thing I'm going to do is register the new archive widget (and unregister the default version). Do so by adding the following mytheme_widget_init() function to functions-widgets.php, then calling it:
/**
* Registering MyTheme Widgets
* Removing a few default WordPress Widgets
*
* @since 1.0
*/
function mytheme_widget_init() {
// unregister some default widgets
unregister_widget('WP_Widget_Archives');
// register my own widgets
register_widget('MyTheme_Widget_Archives');
}
add_action('widgets_init', 'mytheme_widget_init');
After completing the above steps, your post count will show up inside the anchor tag. One final (optional) step is modifying the style.css file so archive links are highlighted when hovering. For the "classic-modified" theme, I'm going to add:
/* Custom styles for archive widget */
.widget_archive,
.widget_archive ul,
.widget_archive ul li,
.widget_archive ul li ul,
.widget_archive ul li ul li {
border: 0px;
margin: 0px;
padding: 0px;
list-style: none;
display: block;
}
.widget_archive ul li a,
.widget_archive ul li ul li a {
display: block;
border-bottom: 1px dotted #666;
margin: 0px;
padding: 0px;
}
.widget_archive ul li a,
.widget_archive ul li a:link,
.widget_archive ul li a:visited,
.widget_archive ul li a:hover,
.widget_archive ul li a:active {
text-decoration: none;
}
.widget_archive ul li a:hover {
background: #eee;
}
BEFORE (default archive widget):
AFTER (modified archive widget):
BUGFIX: Fixed a bug that was ignoring certain .aif/.aiff files.
BUGFIX: Fixed a CPU Memory Leak that occured when using the "Remove All Samples" Fuction. Previously, I was only freeing CPU memory when using the "Remove Currently Selected Sample" or "Remove All Samples Not Used In A Pattern" Fuctions. This could slow down your system (only while the program was running) if you repeatedly used the "Remove All Samples" Fuction in a session.
BUGFIX: Fixed a bug that was not displaying the STEREO Sample Names in the Pattern Organizer.
BUGFIX: Fixed another bug that was not always storing "SLICE" samples correctly. Search the forums for more info about this bug.
Changed the name of the "Pattern Organizer" to "Pattern Editor".
Changed the name of the "Song Organizer" to "Song Editor".
The Song Editor works now.
Added Minimize/Maximize Buttons to the Pattern Editor and the Song Editor.
Added a "first loading slot" feature. Previously, all samples would try to load into slot #000. If that was already in use, it would find the next available slot. Now, you can set the first slot number to try. It will then find the next available slot (looping around when it gets to the end).
Changed the "About Box". Now you can click on my email address to open your default email client to email me, or click on the program's webpage url to open your default webbrowser and visit the homepage.
Removed the "Push Up" and "Push Down" buttons (which weren't implemented), and replaced them with the "NEVER LOOP" and "LOOP WHOLE" buttons.
The "NEVER LOOP" button sets the sample's "Loop Start" value equal to the sample's "End" value. This means that the sample will never loop when played as a keyboard part.
The "LOOP WHOLE" button sets the sample's "Loop Start" value equal to the sample's "Start" value. This means that the sample will loop completely when played as a keyboard part.
The "LOOP" button will only work for MONO samples that have a "Loop Start" value. If a sample doesn't have a "Loop start" value (and you want to hear it loop), you can press the "LOOP WHOLE" button. Now you can press the "LOOP" button.
Changed the "Extract All Waves" feature. Before it would rename files with the sample name last. Now it renames them with the sample name first. So:
BEFORE: "ESXextracted-MONO-140-M1PickBs.wav"
NOW: "M1PickBs-MONO-140-ESXextracted.wav"
Changed the button name "Remove The Selected Wave" to "Remove The Selected Sample".
Changed the button name "Edit The Selected Wave" to "Edit The Selected Sample".
Added a "Convert Selected Sample (MONO/ST)" button. This will convert a Mono Sample to a Stereo Sample, or a Stereo Sample to a Mono Sample (depending on what is selected, and how much free space there is).
BUGFIX: Before I was only accepting files with the extensions (.esx, .wav, and .aif). I was ignoring all other files (including .aiff). Now I am accepting .aiff as well as .aif, so: (.esx, .wav, .aif, and .aiff are all the acceptable formats). Thanks to Jayzilla for reporting this.
Added a "Pattern List" Column in the Sample Organizers, which let you see what Patterns a Sample is used in.
Added a "Remove All Samples Not Used In A Pattern" function. This lets you free up space by removing any samples that aren't being used.
Added a "Mono Organizer" function. This opens up a new dialog window, which will let you select multiple samples, and move them around (by dragging them). Hold down the shift button to select multiple samples. You can rename multiple samples, and change their stretch step values. If you check the "Add Number" box, then you can rename multiple samples, and a unique number will be added to the end of the sample name (for instance: if you type "bass", your samples will be renamed "bass0", "bass1", "bass2", etc.).
Fixed major "SLICE" problem. Before, if you moved a sample that had been sliced on the ESX, it would not play back properly without being "re-sliced". This has now been fixed.
Added "Set All Play Levels To" to the Functions menu. This will let you set them to either 0dB or +12dB.
Fixed typos:
changed "Fuctions" to "Functions"
changed "Exit Part Editor" to "Exit Pattern Editor"
You can now view if a sample has been time sliced or not.
You can now view and set the Play Level for each samples.
BUG FIX == Before, if the focus was on Mono Sample 001, and you clicked on a "greyed" Stereo Sample 001, the focus would still be on Mono Sample 001. This has been fixed. No matter what sample you click on, it will always become the focus...
BUG FIX == Before I accept an .esx file into the workspace, I do a few checks on the file. It must contain the values "KORG" in 2 places; it must contain the value 0x71 in two places; and it must contain the value "ESX" in one place. Previously, I was also making sure that it had the value "BPS" in one place. This was not needed, and it was not true for all .esx files. I found this out because someone emailed me saying that the program was not accepting their .esx file. Where I thought that the value "BPS" was needed, it wasn't. The .esx file in question, had the value "BPA" where I thought the value had to be "BPS". If anyone else has a problem loading an .esx file into the workspace, email me... It is probably because one of the other checks I do is unnecessary.
Added AIFF support. The program will recognize 8 or 16 bit, mono or stereo, .aif files.
Added Minimize and Maximize Buttons.
The Play and Loop buttons work for every sample now. If you press the Loop button on a sample that is not a loop, it will play the whole sample repeatedly.
Rearranged the main dialog. Now the Mono and Stereo lists are side by side (displaying more samples at once).
Got rid of the "quick save" buttons, and added regular "Windows style" open and save dialogs.
Added a "Remove All Samples" feature, which deletes all the samples from the workspace, but leaves the pattern and song data that is loaded.
Got rid of the file "esxwaveorganizer.data". Now you only need the .exe file for the program to work.
Fixed the "Clear Pattern & Song Data". Previously, when you cleared all pattern data, each part's sample would be set to Mono Sample #000. Now, when you clear all pattern data, and load an .esx file into your sampler, each parts' sample will be set to NONE (not Mono Sample #000).
Added a Menu with the options:
File:
Open/Add Files To Workspace
Save As ESX File
Extract All Waves
Exit
Functions:
Clear All Pattern And Song Data
Pattern Organizer
Song Organizer
Remove All Samples
Help:
About ESX Organizer
Visit Website
Also, you cannot set the start, end, or loop start times. You cannot set the "time slice", "auto sampling", or "playback level" parameters. In the future, I hope to implement these features. It depends on how much feedback I get, and how much spare time I have.
There is also a minor bug in Version 0.1 that occurs when switching between the mono and stereo
lists. If you click on a "greyed" selection, the "focus" of the main window will not change.
To test this, open "ESX Wave Organizer". Now click on Mono Sample 003. The "focus" of the main
window is Mono Sample 003. Now click on Stereo Sample 005. The "focus" of the main window is
Stereo Sample 005. Now click on the "greyed" Mono Sample 003 again. The "focus" of the main
window is still Stereo Sample 005. This is not a very big deal, just make sure you check the
"focus" of the window when changing from mono to stereo, or refrain from clicking on the "greyed"
selections.
You can change Midi In and Midi Out devices at any time while running the program.
You can select different Channels for the midi in and out devices.
3 different pattern modes, you can select the starting and ending steps for a pattern
You can load and save presets. (all preset files are 578KB big)
You can select which of the 17 different parameters to send to the ES-1.
You can select which Note Numbers control the different parts on the ES-1.
New Random Generator Features -- You can give an upper and lower bound for 17 different parameters, and select which effects and parts are allowed to be assigned.
Key Settings Dialog -- You can now edit patterns and all their parameters on screen.
Set all function which allows you to set every step for every key to a specified value.
Panic button which automatically turns off all the keys when it is hit.
New GUI
No install program in this version. Just unzip it and run it.
Fixed a minor bug - In old versions, if ES1step could not find a midi input or output device, it would still ask you to enter a device number and then quit when it couldn't find the device. Now it will tell you to download MidiYOKE and MidiOX. ES1step will definately recognize MidiYoke's virtual ports.
Pattern Settings Feature - Now you can set a starting and ending step, and also specify whether or not to loop through the pattern while you hold a key down, or just play the pattern once.
Random Generator Addition - Now you can toggle which effects the random generator can use.
Send These Parameters Function - Now you can select which parameters you want to send to the ES-1. If you want to manually tweak knobs on the ES-1, just use this function to deselect the right parameters.