Creating a Cassette Converter plug-in

Hey guys, I’m trying to create an Audacity plug-in that can fully automate the process of converting cassettes into mp3s. This is actually for my final year project of my IT degree. Here’s the general outline of what I want to do:

  1. User can either enter ID3 tags of tracks before or after the process. When the user presses play on the cassette player, Audacity automatically starts recording when it receives a signal (probably above a certain dB so that it doesn’t start because of noise) and Audacity stops recording when the tape ends.
  2. Noise removal. I need Audacity to automatically detect a gap between tracks, using that as a noise profile, and then apply it to the whole track.
  3. Split the whole recorded track into individual songs. For my prototype I used Sound Finder which seemed to work okay, although sometimes it doesn’t mark the track properly.
  4. Save into files.

For my prototype, I basically used a Chain to apply noise removal and sound finder. The track tagging and the auto record/auto stop needs work. Initially I thought of doing this in Nyquist but some forumers told me Mod Script Pipe might be a better way to go.

The main point of this project is so that Audacity can fully automate the process. As for the quality of the recorded tracks, that’s secondary for now.

Any help will be much appreciated. Thanks!

You seem to be double posting with the same job. Is there a reason you’re doing that?
Koz

Well Steve said it might be more relevant to post this here so… yeah…

I think that Steve’s Auto-it or AutoHotkey idea is still the best.

There are numerous features of Audacity involved that would need proper treatment because they are dependant on the settings in preferences or the export dialog.

I would have designed the task differently:

  1. The user is responsible for the recording.
  2. You deliver the Nyquist plug-in with the following inputs:
  • Album Title
  • Destination for the files
  • the play times for the tracks [and titles].
    There could of course also be an automatic mode be available for cassettes that are custom-compiled.
    The plug-in would do:
  • match and trim the audio to align correctly with the original, i.e. if it were from CD.
  • Gaps are taken as noise profile and do a (somewhat) different noise reduction.
    (Koz did make a hint that Dolby compression could interfere as well)
  • The tracks are exported as *.WAVs or directly as burnable Disk image.

The idea would be to let the player search for the tags when the real or virtual CD is mounted.


I know that this does all not sound very promising.
One should actually write a stand-alone application to fullfille all the premises you’re basing the project on (with all the external libraries that are used).

Hey guys,

So I got the green light from my supervisor to use AutoIt or AutoHotkey. Do you guys have any suggestions on how I could make it work using them?
I did tell him that my only other option is to do a standalone program but since I only have about 9 weeks left till deadline I don’t think I could make it in time…

I’ve recently been playing with “Actionaz” (https://wiki.actiona.tools/doku.php?id=en:start) and found it quite easy to automate tasks in Audacity.
Here is a test Actionaz script that I wrote to automate multiple sequential recordings (record a bit, export it as a file with a unique name, close the current project, record some more in a new project export that, repeat …)

The script was written entirely within the Actionaz graphical interface (no coding required).

<?xml version="1.0" encoding="UTF-8"?>
<scriptfile>
    <settings program="actionaz" version="3.4.2" scriptVersion="1.0.0" os="GNU/Linux"/>
    <actions>
        <action name="ActionVariable" version="1.0.0"/>
        <action name="ActionVariableCondition" version="1.0.0"/>
        <action name="ActionWindow" version="1.0.0"/>
        <action name="ActionWindowCondition" version="1.0.0"/>
        <action name="ActionPause" version="1.0.0"/>
        <action name="ActionPlaySound" version="1.0.0"/>
        <action name="ActionDetachedCommand" version="1.0.0"/>
        <action name="ActionWriteText" version="1.0.0"/>
        <action name="ActionKey" version="1.0.0"/>
    </actions>
    <parameters/>
    <script pauseBefore="250" pauseAfter="0">
        <action name="ActionVariable" comment="Initialise counter">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <parameter name="colorValue">
                <subParameter name="value" code="0">::</subParameter>
            </parameter>
            <parameter name="variable">
                <subParameter name="value" code="0">counter</subParameter>
            </parameter>
            <parameter name="type">
                <subParameter name="value" code="0">integer</subParameter>
            </parameter>
            <parameter name="value">
                <subParameter name="value" code="0">1</subParameter>
            </parameter>
            <parameter name="positionValue">
                <subParameter name="value" code="0">:</subParameter>
            </parameter>
        </action>
        <action name="ActionWindowCondition" comment="Skip to line 008 if already open">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <parameter name="width">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="title">
                <subParameter name="value" code="0">Audacity</subParameter>
            </parameter>
            <parameter name="condition">
                <subParameter name="value" code="0">exists</subParameter>
            </parameter>
            <parameter name="size">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="processId">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="height">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="position">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="ifTrue">
                <subParameter name="line" code="0">008</subParameter>
                <subParameter name="action" code="0">goto</subParameter>
            </parameter>
            <parameter name="ifFalse">
                <subParameter name="line" code="0"></subParameter>
                <subParameter name="action" code="0">do_nothing</subParameter>
            </parameter>
            <parameter name="yCoordinate">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="xCoordinate">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
        </action>
        <action name="ActionDetachedCommand" comment="Launch Audacity">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <parameter name="processId">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="parameters">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="command">
                <subParameter name="value" code="0">/home/steve/sourcecode/audacity/build/audacity</subParameter>
            </parameter>
            <parameter name="workingDirectory">
                <subParameter name="value" code="0">/home/steve/sourcecode/audacity/</subParameter>
            </parameter>
        </action>
        <action name="ActionWindowCondition" comment="Wait until open">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <parameter name="width">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="title">
                <subParameter name="value" code="0">Audacity</subParameter>
            </parameter>
            <parameter name="condition">
                <subParameter name="value" code="0">exists</subParameter>
            </parameter>
            <parameter name="size">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="processId">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="height">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="position">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="ifTrue">
                <subParameter name="line" code="0"></subParameter>
                <subParameter name="action" code="0">do_nothing</subParameter>
            </parameter>
            <parameter name="ifFalse">
                <subParameter name="line" code="0"></subParameter>
                <subParameter name="action" code="0">wait</subParameter>
            </parameter>
            <parameter name="yCoordinate">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
            <parameter name="xCoordinate">
                <subParameter name="value" code="0"></subParameter>
            </parameter>
        </action>
        <action name="ActionWindow" comment="Ensure Audacity is foreground" pauseAfter="100">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="title">
                <subParameter name="value" code="0">Audacity</subParameter>
            </parameter>
            <parameter name="resizeHeight">
                <subParameter name="value" code="0">0</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">setForeground</subParameter>
            </parameter>
            <parameter name="movePosition">
                <subParameter name="value" code="0">:</subParameter>
            </parameter>
            <parameter name="useBorders">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="resizeWidth">
                <subParameter name="value" code="0">0</subParameter>
            </parameter>
        </action>
        <action name="ActionWindow" comment="Ensure screen position is sensible">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="title">
                <subParameter name="value" code="0">Audacity</subParameter>
            </parameter>
            <parameter name="resizeHeight">
                <subParameter name="value" code="0">0</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">move</subParameter>
            </parameter>
            <parameter name="movePosition">
                <subParameter name="value" code="0">320:80</subParameter>
            </parameter>
            <parameter name="useBorders">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="resizeWidth">
                <subParameter name="value" code="0">0</subParameter>
            </parameter>
        </action>
        <action name="ActionWindow" comment="Ensure sensible size">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="title">
                <subParameter name="value" code="0">Audacity</subParameter>
            </parameter>
            <parameter name="resizeHeight">
                <subParameter name="value" code="0">600</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">resize</subParameter>
            </parameter>
            <parameter name="movePosition">
                <subParameter name="value" code="0">:</subParameter>
            </parameter>
            <parameter name="useBorders">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="resizeWidth">
                <subParameter name="value" code="0">800</subParameter>
            </parameter>
        </action>
        <action name="ActionKey" comment="Press R (record)">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="alt">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">pressRelease</subParameter>
            </parameter>
            <parameter name="meta">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="key">
                <subParameter name="key" code="0">R</subParameter>
                <subParameter name="isQtKey" code="0">true</subParameter>
            </parameter>
            <parameter name="ctrl">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="shift">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="type">
                <subParameter name="value" code="0">Win32</subParameter>
            </parameter>
            <parameter name="pause">
                <subParameter name="value" code="0">10</subParameter>
            </parameter>
        </action>
        <action name="ActionPause" comment="DO THE RECORDING">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <parameter name="duration">
                <subParameter name="value" code="0">5</subParameter>
            </parameter>
            <parameter name="unit">
                <subParameter name="value" code="0">seconds</subParameter>
            </parameter>
        </action>
        <action name="ActionKey" comment="Space (stop)" pauseAfter="200">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="alt">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">pressRelease</subParameter>
            </parameter>
            <parameter name="meta">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="key">
                <subParameter name="key" code="0">Space</subParameter>
                <subParameter name="isQtKey" code="0">true</subParameter>
            </parameter>
            <parameter name="ctrl">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="shift">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="type">
                <subParameter name="value" code="0">Win32</subParameter>
            </parameter>
            <parameter name="pause">
                <subParameter name="value" code="0">10</subParameter>
            </parameter>
        </action>
        <action name="ActionKey" comment="Ctrl+E (export)" pauseAfter="200">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="alt">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">pressRelease</subParameter>
            </parameter>
            <parameter name="meta">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="key">
                <subParameter name="key" code="0">E</subParameter>
                <subParameter name="isQtKey" code="0">true</subParameter>
            </parameter>
            <parameter name="ctrl">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="shift">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="type">
                <subParameter name="value" code="0">Win32</subParameter>
            </parameter>
            <parameter name="pause">
                <subParameter name="value" code="0">10</subParameter>
            </parameter>
        </action>
        <action name="ActionWriteText" comment="Enter file name and number">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <parameter name="text">
                <subParameter name="value" code="1">var today = new Date();
var filename = "Recording-" + counter + "_at_" +  today.getHours() + "-" + today.getMinutes();
counter += 1;
filename</subParameter>
            </parameter>
        </action>
        <action name="ActionKey" comment="S (save export file)">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="alt">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">pressRelease</subParameter>
            </parameter>
            <parameter name="meta">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="key">
                <subParameter name="key" code="0">S</subParameter>
                <subParameter name="isQtKey" code="0">true</subParameter>
            </parameter>
            <parameter name="ctrl">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="shift">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="type">
                <subParameter name="value" code="0">Win32</subParameter>
            </parameter>
            <parameter name="pause">
                <subParameter name="value" code="0">10</subParameter>
            </parameter>
        </action>
        <action name="ActionPause" comment="SAVE THE FILE" pauseBefore="500">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <parameter name="duration">
                <subParameter name="value" code="0">2</subParameter>
            </parameter>
            <parameter name="unit">
                <subParameter name="value" code="0">milliseconds</subParameter>
            </parameter>
        </action>
        <action name="ActionKey" comment="Ctrl+W (close)">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="alt">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">pressRelease</subParameter>
            </parameter>
            <parameter name="meta">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="key">
                <subParameter name="key" code="0">W</subParameter>
                <subParameter name="isQtKey" code="0">true</subParameter>
            </parameter>
            <parameter name="ctrl">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="shift">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="type">
                <subParameter name="value" code="0">Win32</subParameter>
            </parameter>
            <parameter name="pause">
                <subParameter name="value" code="0">10</subParameter>
            </parameter>
        </action>
        <action name="ActionKey" comment="Alt+N (don't save project)" pauseAfter="100">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="alt">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">pressRelease</subParameter>
            </parameter>
            <parameter name="meta">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="key">
                <subParameter name="key" code="0">N</subParameter>
                <subParameter name="isQtKey" code="0">true</subParameter>
            </parameter>
            <parameter name="ctrl">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="shift">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="type">
                <subParameter name="value" code="0">Win32</subParameter>
            </parameter>
            <parameter name="pause">
                <subParameter name="value" code="0">10</subParameter>
            </parameter>
        </action>
        <action name="ActionPause" comment="May need a pause here for long recordings" enabled="false">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <parameter name="duration">
                <subParameter name="value" code="0">100</subParameter>
            </parameter>
            <parameter name="unit">
                <subParameter name="value" code="0">milliseconds</subParameter>
            </parameter>
        </action>
        <action name="ActionVariableCondition" comment="Loop back to line 002">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <parameter name="ifEqual">
                <subParameter name="line" code="0">002</subParameter>
                <subParameter name="action" code="0">goto</subParameter>
            </parameter>
            <parameter name="comparison">
                <subParameter name="value" code="0">inferior</subParameter>
            </parameter>
            <parameter name="variable">
                <subParameter name="value" code="0">counter</subParameter>
            </parameter>
            <parameter name="value">
                <subParameter name="value" code="0">5</subParameter>
            </parameter>
            <parameter name="ifDifferent">
                <subParameter name="line" code="0"></subParameter>
                <subParameter name="action" code="0">do_nothing</subParameter>
            </parameter>
        </action>
        <action name="ActionKey" comment="Ctrl+Q (quit Audacity)" pauseBefore="400">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <exception id="32" action="0" line=""/>
            <exception id="33" action="0" line=""/>
            <parameter name="alt">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="action">
                <subParameter name="value" code="0">pressRelease</subParameter>
            </parameter>
            <parameter name="meta">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="key">
                <subParameter name="key" code="0">Q</subParameter>
                <subParameter name="isQtKey" code="0">true</subParameter>
            </parameter>
            <parameter name="ctrl">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="shift">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="type">
                <subParameter name="value" code="0">Win32</subParameter>
            </parameter>
            <parameter name="pause">
                <subParameter name="value" code="0">10</subParameter>
            </parameter>
        </action>
        <action name="ActionPlaySound">
            <exception id="0" action="0" line=""/>
            <exception id="1" action="0" line=""/>
            <exception id="2" action="1" line=""/>
            <parameter name="url">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="looping">
                <subParameter name="value" code="0">false</subParameter>
            </parameter>
            <parameter name="volume">
                <subParameter name="value" code="0">100</subParameter>
            </parameter>
            <parameter name="file">
                <subParameter name="value" code="0">/home/steve/Music/KeepCalmCarryOn-mix2.flac</subParameter>
            </parameter>
            <parameter name="blocking">
                <subParameter name="value" code="0">true</subParameter>
            </parameter>
            <parameter name="playbackRate">
                <subParameter name="value" code="0">100</subParameter>
            </parameter>
        </action>
    </script>
</scriptfile>

Oh wow that’s really helpful thanks Steve! But what about the bigger picture though. Should I create a chain with all the processes i need in Audacity first and then use Actionaz to automate?

Hey guys quick question. I’m having problems with Sound Finder. It’s detecting gaps between songs as a track as well. Are there any fixes/alternatives?

I have posted several alternative versions of Sound Finder here on the forum - several with more “advanced” controls.
The forum search function is pretty limited, so try a Google search with something like:

site:http://forum.audacityteam.org "sound finder"

Also this may sound like a dumb question, but how do i get Audacity to export the regions marked by sound finder into separate files?

Not at all dumb. See “Export Multiple” in the manual (direct link in online version: Audacity Manual)

Hey guys,

Is there a way to make Audacity stop recording automatically when the tape ends? Like maybe if there’s silence for more than say… 5 seconds, Audacity just stops recording.

Not currently. Audacity can be made to stop immediately when the sound drops below a specified level (see Sound Activated Recording), but does not have a 5 second delay.

So I’m guessing, that once Sound Finder marks the tracks, Export Multiple here will use the marks set by Sound Finder as labels?

Also, I’m not really sure how to fit Actionaz into this. Initially I thought of creating a chain with noise reduction, then sound finder to mark the tracks, and then if possible exporting multiple in the chain itself. But if I do that, I’m not sure what to do with Actionaz.

Guys any suggestions on how I should proceed?

Okay last question. Is it possible to put export multiple into a chain? And if it is, how?
And also, is it possible to use Silence Finder to mark the silences (which are set as below a certain dB) and then using those marked regions as a noise profile for noise reduction? OR maybe immediately removing silences found by Silence Finder, and then consolidating, so that the gaps between tracks will be absolutely quiet (therefore no noise). At this point I’m only interested in the noise between tracks, not in the track itself.

The project’s almost due so these are basically the last questions I’ll be asking. :slight_smile:

We can probably answer most of your technical questions, or at least point you in the direction of an answer, but I don’t think that we can tell you how to invent something that you want to invent.

No, export multiple is not supported in Chains.

Not automatically. You would normally select the “noise region” with your mouse, though if those regions were marked with region labels you could tab across the labels (move focus into the label track, then press the Tab key).

So my only option is to manually click on Export Multiple after the processes are done? Is there no way to make Audacity immediately prompt where to save to right after the processes are done? Also, when exporting multiple, after tagging the first track is there a way to make the 2nd track retain say the artist, album, year? Just so that users won’t have to key in the same thing again, making it redundant.

I see. What about the deleting gaps between tracks and then consolidating them again? Is that possible?

Some of the tasks that you want to automate cannot be automated within Audacity. That is why I suggested using a third party scripting application such as Actionaz, Autokey, AutoHotKeys, Applescript, or similar.