トレモロによるオートパン (オーディオ) | エフェクター

LOOP
ChannelSplitterNode
ChannelSplitterNode
PropertyValuehasOwnProperty
ChannelMergerNode
ChannelMergerNode
PropertyValuehasOwnProperty
(function() {

    var onDOMContentLoaded = function() {

        window.AudioContext = window.AudioContext || window.webkitAudioContext;

        try {
            // Create the instance of AudioContext
            var context = new AudioContext();
        } catch (error) {
            window.alert(error.message + ' : Please use Chrome or Safari.');
            return;
        }

        var displayProperties = function(node, tableid, caption) {
            var html = '<caption>' + caption + '</caption>';

            html += '<thead>';
            html += '<tr>';
            html += '<th scope="col">Property</th>';
            html += '<th scope="col">Value</th>';
            html += '<th scope="col">hasOwnProperty</th>';
            html += '</tr>';
            html += '</thead>';

            html += '<tbody>';

            for (var key in node) {
                html += '<tr>';
                html += '<td>' + key + '</td>';
                html += '<td>' + node[key] + '</td>';
                html += '<td>' + node.hasOwnProperty(key) + '</td>';
                html += '</tr>';
            }

            html += '</tbody>';

            document.getElementById(tableid).innerHTML = html;
            document.getElementById(tableid).parentNode.previousElementSibling.style.display = 'block';
        };

        // for the instance of AudioBufferSourceNode
        var source = null;

        // for legacy browsers
        context.createGain = context.createGain || context.createGainNode;

        // Create the instance of GainNode (for Master Volume)
        var gain = context.createGain();

        // Auto Panner

        // Create the instance of OscillatorNode (for LFO)
        var lfo = context.createOscillator();

        // for leagcy browsers
        lfo.start = lfo.start || lfo.noteOn;
        lfo.stop  = lfo.stop  || lfo.noteOff;

        // Create the instance of ChannelSplitterNode for LFO
        var lfoSplitter = context.createChannelSplitter(2);

        // for legacy browsers
        context.createScriptProcessor = context.createScriptProcessor || context.createJavaScriptNode;

        // for selecting optimized buffer size
        var getBufferSize = function() {
            if (/(Win(dows )?NT 6\.2)/.test(navigator.userAgent)) {
                return 1024;  // Windows 8
            } else if (/(Win(dows )?NT 6\.1)/.test(navigator.userAgent)) {
                return 1024;  // Windows 7
            } else if (/(Win(dows )?NT 6\.0)/.test(navigator.userAgent)) {
                return 2048;  // Windows Vista
            } else if (/Win(dows )?(NT 5\.1|XP)/.test(navigator.userAgent)) {
                return 4096;  // Windows XP
            } else if (/Mac|PPC/.test(navigator.userAgent)) {
                return 1024;  // Mac OS X
            } else if (/Linux/.test(navigator.userAgent)) {
                return 8192;  // Linux
            } else if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
                return 2048;  // iOS
            } else {
                return 16384;  // Otherwise
            }
        };

        // This callback is executed in order to reverse amplitude each channel
        var onaudioprocessCallback =function(event) {
            // Get the instance of Float32Array for input data (Array size equals buffer size)
            var inputLs = event.inputBuffer.getChannelData(0);  // Left  channel
            var inputRs = event.inputBuffer.getChannelData(1);  // Right channel

            // Get the instance of Float32Array for output data (Array size equals buffer size)
            var outputLs = event.outputBuffer.getChannelData(0);  // Left  channel
            var outputRs = event.outputBuffer.getChannelData(1);  // Right channel

            // Reverse amplitude each channel
            for (var i = 0; i < this.bufferSize; i++) {
                outputLs[i] =  1 * inputLs[i];  // GainNode.gain (1) + outputLs[i]
                outputRs[i] = -1 * inputRs[i];  // GainNode.gain (1) - outputLs[i]
            }
        };

        // Create the instance of ScriptProcessorNode for LFO
        var processor = context.createScriptProcessor(getBufferSize(), 2, 2);

        // for Auto Panner parameters
        var amplitudeL = context.createGain();
        var amplitudeR = context.createGain();
        var splitter   = context.createChannelSplitter(2);  // Create the instance of ChannelSplitterNode for original sound
        var merger     = context.createChannelMerger(2);    // Create the instance of ChannelMergerNode for original sound
        var depth      = context.createGain();
        var rate       = lfo.frequency;

        // Initialize parameters for Auto Panner
        amplitudeL.gain.value = 1;  // 1 +- Depth
        amplitudeR.gain.value = 1;  // 1 +- Depth
        depth.gain.value      = document.getElementById('range-autopanner-depth').valueAsNumber;
        rate.value            = document.getElementById('range-autopanner-rate').valueAsNumber;

        var stopLFO = function() {
            // Stop LFO
            lfo.stop(0);

            // Create the instance of OscillatorNode (for LFO)
            lfo = context.createOscillator();

            // for leagcy browsers
            lfo.start = lfo.start || lfo.noteOn;
            lfo.stop  = lfo.stop  || lfo.noteOff;

            // Set Rate
            lfo.frequency.value = rate.value;

            // Change reference
            rate = lfo.frequency;

            // Pause onaudioprocess event
            processor.disconnect(0);
            processor.onaudioprocess = null;
        };

        // Trigger 'ended' event
        var trigger = function() {
            var event = document.createEvent('Event');
            event.initEvent('ended', true, true);

            if (source instanceof AudioBufferSourceNode) {
                source.dispatchEvent(event);
            }
        };

        // This funciton is executed after getting ArrayBuffer of audio data
        var startAudio = function(arrayBuffer) {

            // The 2nd argument for decodeAudioData
            var successCallback = function(audioBuffer) {
                // The 1st argument (audioBuffer) is the instance of AudioBuffer

                // If there is previous AudioBufferSourceNode, program stops previous audio
                if ((source instanceof AudioBufferSourceNode) && (source.buffer instanceof AudioBuffer)) {
                    // Execute onended event handler
                    trigger();
                    source = null;
                }

                // Create the instance of AudioBufferSourceNode
                source = context.createBufferSource();

                // for legacy browsers
                source.start = source.start || source.noteOn;
                source.stop  = source.stop  || source.noteOff;

                // Set the instance of AudioBuffer
                source.buffer = audioBuffer;

                // Set parameters
                source.playbackRate.value = document.getElementById('range-playback-rate').valueAsNumber;
                source.loop               = document.querySelector('[type="checkbox"]').checked;

                // GainNode (Master Volume) -> AudioDestinationNode (Output)
                gain.connect(context.destination);

                // Clear connection
                source.disconnect(0);
                splitter.disconnect(0);
                splitter.disconnect(1);
                amplitudeL.disconnect(0);
                amplitudeR.disconnect(0);
                merger.disconnect(0);

                if (document.getElementById('toggle-effect').checked) {
                    // Auto Panner ON

                    // Connect nodes for effect (Auto Panner) sound
                    //                                                       -> GainNode for Left  Channel (Amplitude) -
                    // AudioBufferSourceNode (Input) -> ChannelSplitterNode -|                                           |-> ChannelMergerNode -> GainNode (Master Volume) (-> AudioDestinationNode (Output))
                    //                                                       -> GainNode for Right Channel (Amplitude) -
                    source.connect(splitter);
                    splitter.connect(amplitudeL, 0, 0);
                    splitter.connect(amplitudeR, 1, 0);
                    amplitudeL.connect(merger, 0, 0);
                    amplitudeR.connect(merger, 0, 1);
                    merger.connect(gain);

                    processor.onaudioprocess = onaudioprocessCallback;
                } else {
                    // Auto Panner OFF

                    // AudioBufferSourceNode (Input) -> GainNode (Master Volume) (-> AudioDestinationNode (Output))
                    source.connect(gain);
                }

                // Connect nodes for LFO that changes Amplitude periodically
                //                                                                                                         -> gain (GainNode for Left  Channel)
                // OscillatorNode (LFO) -> GainNode (Depth) -> ScriptProcessorNode (Reverse Amplitude) -> ChannelSplitter -|
                //                                                                                                         -> gain (GainNode for Right Channel)
                lfo.connect(depth);
                depth.connect(processor);
                processor.connect(lfoSplitter);
                lfoSplitter.connect(amplitudeL.gain, 0);
                lfoSplitter.connect(amplitudeR.gain, 1);

                // Start audio
                source.start(0);

                // Start LFO
                lfo.start(0);

                // Set Callback
                source.onended = function(event) {
                    // Remove event handler
                    source.onended     = null;
                    document.onkeydown = null;

                    // Stop audio
                    source.stop(0);

                    // Stop LFO
                    stopLFO();

                    console.log('STOP by "on' + event.type + '" event handler !!');

                    // Audio is not started !!
                    // It is necessary to create the instance of AudioBufferSourceNode again

                    // Cannot replay
                    // source.start(0);
                };

                // Stop audio
                document.onkeydown = function(event) {
                    // Space ?
                    if (event.keyCode !== 32) {
                        return;
                    }

                    // Execute onended event handler
                    trigger();

                    return false;
                };
            };

            // The 3rd argument for decodeAudioData
            var errorCallback = function(error) {
                if (error instanceof Error) {
                    window.alert(error.message);
                } else {
                    window.alert('Error : "decodeAudioData" method.');
                }
            };

            // Create the instance of AudioBuffer (Asynchronously)
            context.decodeAudioData(arrayBuffer, successCallback, errorCallback);
        };

        /*
         * File Uploader
         */

        document.getElementById('file-upload-audio').addEventListener('change', function(event) {
            var uploader     = this;
            var progressArea = document.getElementById('progress-file-upload-audio');

            // Get the instance of File (extends Blob)
            var file = event.target.files[0];

            if (!(file instanceof File)) {
                window.alert('Please upload file.');
            } else if (file.type.indexOf('audio') === -1) {
                window.alert('Please upload audio file.');
            } else {
                // Create the instance of FileReader
                var reader = new FileReader();

                reader.onprogress = function(event) {
                    if (event.lengthComputable && (event.total > 0)) {
                        var rate = Math.floor((event.loaded / event.total) * 100);
                        progressArea.textContent = rate + ' %';
                    }
                };

                reader.onerror = function() {
                    window.alert('FileReader Error : Error code is ' + reader.error.code);
                    uploader.value = '';
                };

                // Success read
                reader.onload = function() {
                    var arrayBuffer = reader.result;  // Get ArrayBuffer

                    startAudio(arrayBuffer);

                    uploader.value           = '';
                    progressArea.textContent = file.name;
                };

                // Read the instance of File
                reader.readAsArrayBuffer(file);
            }
        }, false);

        // Control Master Volume
        document.getElementById('range-volume').addEventListener('input', function() {
            var min = gain.gain.minValue || 0;
            var max = gain.gain.maxValue || 1;

            if ((this.valueAsNumber >= min) && (this.valueAsNumber <= max)) {
                gain.gain.value = this.valueAsNumber;
                document.getElementById('output-volume').textContent = this.value;
            }
        }, false);

        // Control playbackRate
        document.getElementById('range-playback-rate').addEventListener('input', function() {
            if (source instanceof AudioBufferSourceNode) {
                var min = source.playbackRate.minValue || 0;
                var max = source.playbackRate.maxValue || 1024;

                if ((this.valueAsNumber >= min) && (this.valueAsNumber <= max)) {
                    source.playbackRate.value = this.valueAsNumber;
                }
            }

            document.getElementById('output-playback-rate').textContent = this.value;
        }, false);

        // Toggle loop
        document.querySelector('[type="checkbox"]').addEventListener(EventWrapper.CLICK, function() {
            if (source instanceof AudioBufferSourceNode) {
                source.loop = this.checked;
            }
        }, false);

        // Toggle Effect
        document.getElementById('toggle-effect').addEventListener(EventWrapper.CLICK, function() {
            if (!(source instanceof AudioBufferSourceNode)) {
                return;
            }

            // Clear connection
            source.disconnect(0);
            splitter.disconnect(0);
            splitter.disconnect(1);
            amplitudeL.disconnect(0);
            amplitudeR.disconnect(0);
            merger.disconnect(0);

            if (this.checked) {
                // Auto Panner ON

                // Connect nodes for effect (Auto Panner) sound
                //                                                       -> GainNode for Left  Channel (Amplitude) -
                // AudioBufferSourceNode (Input) -> ChannelSplitterNode -|                                           |-> ChannelMergerNode -> GainNode (Master Volume) (-> AudioDestinationNode (Output))
                //                                                       -> GainNode for Right Channel (Amplitude) -
                source.connect(splitter);
                splitter.connect(amplitudeL, 0, 0);
                splitter.connect(amplitudeR, 1, 0);
                amplitudeL.connect(merger, 0, 0);
                amplitudeR.connect(merger, 0, 1);
                merger.connect(gain);

                // Resume onaudioprocess event
                processor.connect(lfoSplitter);
                processor.onaudioprocess = onaudioprocessCallback;
            } else {
                // Auto Panner OFF

                // AudioBufferSourceNode (Input) -> GainNode (Master Volume) (-> AudioDestinationNode (Output))
                source.connect(gain);

                // Pause onaudioprocess event
                processor.disconnect(0);
                processor.onaudioprocess = null;
            }
        }, false);

        // Control Auto Panner Depth
        document.getElementById('range-autopanner-depth').addEventListener('input', function() {
            depth.gain.value = this.valueAsNumber;
            document.getElementById('output-autopanner-depth').textContent = this.value;
        }, false);

        // Control Auto Panner Rate
        document.getElementById('range-autopanner-rate').addEventListener('input', function() {
            rate.value = this.valueAsNumber;
            document.getElementById('output-autopanner-rate').textContent = this.value;
        }, false);

        /*
         * Display properties
         */

        displayProperties(splitter, 'channelsplitternode-properties', 'ChannelSplitterNode');
        displayProperties(merger,   'channelmergernode-properties',   'ChannelMergerNode');
    };

    if ((document.readyState === 'interactive') || (document.readyState === 'complete')) {
        onDOMContentLoaded();
    } else {
        document.addEventListener('DOMContentLoaded', onDOMContentLoaded, true);
    }

})();
function EventWrapper(){
}

(function(){
    var click = '';
    var start = '';
    var move  = '';
    var end   = '';

    // Touch Panel ?
    if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
        click = 'click';
        start = 'touchstart';
        move  = 'touchmove';
        end   = 'touchend';
    } else {
        click = 'click';
        start = 'mousedown';
        move  = 'mousemove';
        end   = 'mouseup';
    }

    EventWrapper.CLICK = click;
    EventWrapper.START = start;
    EventWrapper.MOVE  = move;
    EventWrapper.END   = end;
})();