
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
window.AudioContext=window.AudioContext || window.webAudioContext || window.webkitAudioContext;


const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

//Jsonic超声波发送数据
$Metia={}

$Metia.global={};
$Metia.global.recognition
$Metia.global.recognitionList={}


$Metia.echo=function({text,lang='zh-cn',voiceURI,volume,rate,pitch})
{
    // text – 要合成的文字内容，字符串。
    // lang – 使用的语言，字符串， 例如："zh-cn"
    // voiceURI – 指定希望使用的声音和服务，字符串。
    // volume – 声音的音量，区间范围是0到1，默认是1。
    // rate – 语速，数值，默认值是1，范围是0.1到10，表示语速的倍数，例如2表示正常语速的两倍。
    // pitch – 表示说话的音高，数值，范围从0（最小）到2（最大）。默认值为1。
    let si=new SpeechSynthesisUtterance(text)
    si.text=text
    if(lang)si.lang=lang
    if(voiceURI)si.voiceURI=voiceURI
    if(volume)si.volume=volume
    if(rate)si.rate=rate
    if(pitch)si.pitch=pitch

    speechSynthesis.speak(si)

    //SpeechSynthesisUtterance
    // onstart – 语音合成开始时候的回调。
    // onpause – 语音合成暂停时候的回调。
    // onresume – 语音合成重新开始时候的回调。
    // onend – 语音合成结束时候的回调。

    //speechSynthesis
    //cancel() – 立即终止合成过程。
    //pause() – 暂停合成过程。
    //resume() – 重新开始合成过程。
    //getVoices() - 用来返回浏览器支持的语音包列表,数组对象中的字段，可用来设置 voiceURI或lang


    return si
}



$Metia.listen=function({continuous=true,next=true,lang='zh-cn',interimResults=false,start=true,fun})
{
    if($Metia.global.recognition)$Metia.global.recognition.stop()
    var recognition = new SpeechRecognition();
    $Metia.global.recognition=recognition

    if(continuous!=undefined)recognition.continuous = continuous;
    if(lang!=undefined)recognition.lang=lang
    if(interimResults!=undefined)recognition.interimResults = interimResults;
    if(start)recognition.start();

    var saidWord
    recognition.onresult = function(event)
    {
        var resultsLength = event.results.length -1 ;
        var ArrayLength = event.results[resultsLength].length -1;
        saidWord = event.results[resultsLength][ArrayLength].transcript;
        if(fun)fun(event.isTrusted,saidWord)
    }
    
    return new Promise
    (
        (res,rej)=>
        {
            recognition.onend = function(event)
            {
                res({isTrusted:event.isTrusted,data:saidWord})
            }
            recognition.onerror = function (error)
            {
                rej(error)
            }
        }
    )
}

$Metia.listenMap=function(name,value)
{
    //value={value,fun,isTrusted,contain,reg}
    $Metia.global.recognitionList[name]=value
}

$Metia.listenFor=async function(obj={})
{
    if(!SpeechRecognition)
    {
        console.log('不支持语音识别');
        return
    }
    while (true) 
    {
        try 
        {
            let param=await   $Metia.listen(obj)  
           
            for (let iterator in $Metia.global.recognitionList) 
            {  
                iterator=$Metia.global.recognitionList[iterator]
                if(iterator.isTrusted)
                {
                    if(!param.isTrusted)continue
                }
                if(iterator.contain)
                {
                    if(!param.data.includes(iterator.value))continue
                }
                else if(iterator.reg)
                {
                    if(!value.test(param.data))continue
                }
                else
                {
                    if(param.data!=iterator.value)continue
                }
                if(iterator.fun)iterator.fun(param)
            }
        } 
        catch (error) 
        {
            $M.info('语音识别处理出错',error)
        }
    }
}

$Metia.init=function(opt={})
{
    //只录取音频，可将opt设为{video:undefined}
    let constraints = 
    {
        "audio": {
            "mandatory": {
                "googEchoCancellation": true,
                "googAutoGainControl": true,
                "googNoiseSuppression": true,
                "googHighpassFilter": true
            },
            "optional": []
        }, 
        "video": 
        {  
            "mandatory": 
            {  
                "minWidth": 640,  
                "maxWidth": 640, 
                "minHeight": 480,
                "maxHeight": 480,
                "frameRate": { ideal: 10, max: 15 },//帧率
                "facingMode": "user" //user前置摄像头,environment后置
            }, 
            "optional": [] 
        } 
    };
    constraints={...constraints,...opt}
    if (!navigator.getUserMedia) 
    {
        alert('MediaRecorder not supported on your browser, use Firefox 30 or Chrome 49 instead.');
        return;
    }
    return new Promise
    (
        (res,rej)=>
        {
            if(constraints.file)
            {
                let type=constraints.video?'video':'audio';
                let video=document.createElement(type);
                video.src=constraints.file;
                res(video)
                return
            }
            navigator.getUserMedia
            (
                constraints, 
                function(stream)
                {
                    res(stream);
                }, 
                function(e)
                {
                    rej(e);
                }
            );
        }
    )
}

$Metia.closeStream=function(stream)
{
    if(!stream)
    {
        $M.info('meta不存在，不能关闭',stream)
        return
    }
    stream.getTracks().forEach
    (
        function (track) 
        {
            track.stop()
        }
    )
}


//拍照
//<input type="file" accept="image/*" onchange='xx' capture="camera" id="upload">  
$Metia.capture= function(video,canvas,fun)
{
    video = $M.find(video);
    canvas = $M.find(canvas);
    $Metia.init({audio:undefined}).then
    (
        (stream)=>
        {
            this.stream=stream;
            this.video=video;
            this.canvas=canvas;
            video.src = window.webkitURL.createObjectURL(stream);
        }
    ).catch
    (
        (err)=>
        {
            if(fun)fun(err)
            console.log(err)
        }
    )
}

$Metia.capture.prototype.get=function({x=0,y=0,width,height,obj})
{
    let canvas = this.canvas.getContext('2d');
    canvas.drawImage(this.video, 0, 0);
    return $Image.clip({canvas:this.canvas,x,y,width,height,obj});
}

$Metia.audio=function(audio,buffer,chan=2)
{
    let context = window.AudioContext
    if(!context)
    {
        alert('浏览器不支持此功能')
        return
    }
    

    let media
    if(audio instanceof OscillatorNode)
    {
        media=audio
        context=media.context
    }
    else 
    {
        context=new context()
        if(audio.tagName)media=context.createMediaElementSource(audio);
        else media=context.createMediaStreamSource(audio);
    }

    
    // bufferSize
    // 缓冲区大小，以样本帧为单位。具体来讲，缓冲区大小必须去是下面这些值当中的某一个: 256, 512, 1024, 2048, 4096, 8192, 16384. 如果不传，或者参数为0，则取当前环境最合适的缓冲区大小, 取值为2的幂次方的一个常数，在该node的整个生命周期中都不变.
    // 该取值控制着audioprocess事件被分派的频率，以及每一次调用多少样本帧被处理. 较低bufferSzie将导致一定的延迟。较高的bufferSzie就要注意避免音频的崩溃和故障。推荐作者不要给定具体的缓冲区大小，让系统自己选一个好的值来平衡延迟和音频质量。
    // numberOfInputChannels
    // 值为整数，用于指定输入node的声道的数量，默认值是2，最高能取32.
    // numberOfOutputChannels
    // 值为整数，用于指定输出node的声道的数量，默认值是2，最高能取32. 
    let processor=context.createScriptProcessor(buffer,chan,chan);
    buffer=processor.bufferSize

    let analyser=context.createAnalyser();
    //表示（信号）样本的窗口大小。当执行快速傅里叶变换（Fast Fourier Transfor (FFT)）时，这些（信号）样本被用来获取频域数据。
    //fftSize 属性的值必须是从32到32768范围内的2的非零幂; 其默认值为2048.
    analyser.fftSize = buffer;
    media.connect(analyser);
    media.connect(processor);
    analyser.connect(context.destination);//把分析节点重定向到输出节点

    this.context=context
    this.analyser=analyser
    this.processor=processor
    this.media=media
    this.buffer=buffer
    this.chan=chan

}

$Metia.audio.prototype.getFrequency=function()
{
    var buf = new Uint8Array(this.analyser.frequencyBinCount);
    this.analyser.getByteFrequencyData(buf);
    return buf
}

$Metia.audio.prototype.getRate=function()
{
    const sampleRate = this.context.sampleRate
    const buf = new Float32Array(this.analyser.frequencyBinCount);
    this.analyser.getFloatTimeDomainData(buf);

    var SIZE = buf.length;
    var MAX_SAMPLES = Math.floor(SIZE/2);
    var best_offset = -1;
    var best_correlation = 0;
    var rms = 0;
    var foundGoodCorrelation = false;
    var correlations = new Array(MAX_SAMPLES);
    for (var i=0;i<SIZE;i++) 
    {
        var val = buf[i];
        rms += val*val;
    }
    rms = Math.sqrt(rms/SIZE);
    if (rms<0.01)return -1;
        
    var lastCorrelation=1;
    for (var offset = 0; offset < MAX_SAMPLES; offset++) 
    {
        var correlation = 0;
        for (var i=0; i<MAX_SAMPLES; i++) 
        {
            correlation += Math.abs((buf[i])-(buf[i+offset]));
        }
        correlation = 1 - (correlation/MAX_SAMPLES);
        correlations[offset] = correlation; 
        
        if ((correlation>0.9) && (correlation > lastCorrelation)) 
        {
            foundGoodCorrelation = true;
            if (correlation > best_correlation) 
            {
                best_correlation = correlation;
                best_offset = offset;
            }
        } 
        else if (foundGoodCorrelation) 
        {
            var shift = (correlations[best_offset+1] - correlations[best_offset-1])/correlations[best_offset];
            return sampleRate/(best_offset+(8*shift));
        }
        lastCorrelation = correlation;
    }
    if (best_correlation > 0.01) 
    {
        return sampleRate/best_offset;
    }
    return -1;
}

$Metia.audio.prototype.getWave=function(fun)
{
    this.processor.onaudioprocess=(el)=>
    {
        //let data=el.inputBuffer.getChannelData(0);//获取输入音频的那个通道
        //data.length//里面的数据
        fun(el.inputBuffer,el.outputBuffer)
    }
}


$Metia.audioSend=function(rate=2000,type='sine')
{
    let context = window.AudioContext
    if(!context)
    {
        alert('浏览器不支持此功能')
    }
    context=new context()

    let osc  = context.createOscillator();
    //'sine'正玄,'square'方波，'triangle'三角波,'sawtooth'锯齿波
    osc.type=type
    //多少hz的声音
    osc.frequency.value = rate;
    osc.connect(context.destination);
    osc.start(0);
    osc.context=context;
    //osc.stop(),调用后不能重新start
    return osc
}

$Metia.audioSendGain=function(arr=[],rate=2000,type='sine')
{
    let context = window.AudioContext
    if(!context)
    {
        alert('浏览器不支持此功能')
    }
    context=new context()
    

    let osc  = context.createOscillator();
    let gain = context.createGain();

    osc.type=type;
    osc.frequency.value = rate;
    
    //控制信号的强弱。值取值范围0~1
    gain.gain.value=0;
    osc.connect(gain); 
    gain.connect(context.destination); 

    osc.start(0);

    //在1-2秒这时间区间内发出一个rate/hz的声波信号
    for (let index = 0; index < arr.length; index++) 
    {
        let cur=arr[index]
        gain.gain.setValueAtTime(cur.value,cur.time); 
    }
    osc.context=context;
    return osc
}



$Metia.recording=function({stream,time,fun}) 
{
    //time每多少毫秒分一次片,如未设置则整个媒体持续时间被记录为单个Blob除非requestData()调用该方法以获得Blob并且触发创建Blob继续记录媒体的新媒体。

    let mediaRecorder;
    let chunks;
    if (typeof MediaRecorder.isTypeSupported == 'function')
    {
        let options
        //这里涉及到视频的容器以及编解码参数，这个与浏览器有密切的关系
        if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) 
        {
          options = {mimeType: 'video/webm;codecs=h264'};
        } 
        else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) 
        {
          options = {mimeType: 'video/webm;codecs=h264'};
        } 
        else  if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) 
        {
          options = {mimeType: 'video/webm;codecs=vp8'};
        }
        mediaRecorder = new MediaRecorder(stream, options);
    }
    else
    {
        //isTypeSupported is not supported, using default codecs for browser
        mediaRecorder = new MediaRecorder(stream);
    }

    mediaRecorder.start(time);
    
    //这个地方，是视频数据捕获好了后，会触发MediaRecorder一个dataavailable的Event，在这里做视频数据的采集工作，
    //主要是基于Blob进行转写，利用FileReader进行读取。FileReader一定
    //要注册loadend的监听器，或者写onload的函数。在loadend的监听函数里面，
    //进行格式转换，方便websocket进行数据传输，因为websocket的数据类型支持blob以及arrayBuffer，我们这里用
    //的是arrayBuffer，所以，将视频数据的Blob转写为Unit8Buffer，便于websocket的后台服务用ByteBuffer接收。
    mediaRecorder.ondataavailable = function(e) 
    {
        let reader = new FileReader();
        reader.addEventListener
        (
            "loadend", function() 
            {
                if(!reader.result.byteLength)return
                //reader.result是一个含有视频数据流的Blob对象
                let buf = new Uint8Array(reader.result);   
                //加这个判断，是因为有很多数据是空的，这个没有必要发到后台服务器，减轻网络开销，提升性能吧。
                if(fun)fun('data',reader.result);
                console.log('收到数据')
            }
        );
        reader.readAsArrayBuffer(e.data);
        chunks.push(e.data);
    };

    //mediaRecorder.start();继续
    //mediaRecorder.stop();停止
    //mediaRecorder.resume();继续
    //mediaRecorder.requestData()；会引发dataavailable事件，就像调用该方法时一样。然后可以根据需要抓取和操纵它。
    //包含到目前为止收到的数据（或自上次requestData()调用以来)
    mediaRecorder.onerror = function(e)
    {
        fun('error',e);  
    };


    mediaRecorder.onstart = function()
    {
        fun('start');
    };

    mediaRecorder.onstop = function()
    {
        //把录制的媒体转为二进制对象
        let blob = new Blob(chunks, {type: "video/webm"});
        chunks = [];

        fun('stop');
    };

    mediaRecorder.onpause = function()
    {
        fun('pause');
    }

    mediaRecorder.onresume = function()
    {
        fun('resume');
    }

    mediaRecorder.onwarning = function(e)
    {
        fun('warning',e);
    };
}


