retrotalk 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ /*! RetroTalk v2.1.0 | MIT License | https://github.com/harayoki/retrotalk */
2
+ (()=>{var bt=class{constructor(t,e={}){this.ctx=t||null,this.seVoices=[],this.maxVoices=e.maxVoices??8,this.bgmVoices=0,this._seSeq=0,this._sePausedAll=!1,this._keepSec=0}unlock(){if(!this.ctx)throw new Error("SimpleAudio: AudioContext を渡してください — new TalkAudio(new AudioContext())");this.ctx.state==="suspended"&&this.ctx.resume()}_startTap(){}_cleanupSE(t){this.seVoices=this.seVoices.filter(e=>{if(t<e.endTime||e.left!==0&&e.timer||!e.system&&(e.paused||this._sePausedAll))return!0;e.timer&&(clearTimeout(e.timer),e.timer=0);try{e.gain.disconnect()}catch{}return!1})}_usedVoices(){return this.bgmVoices+this.seVoices.reduce((t,e)=>t+e.voices,0)}_stopVoice(t){t.timer&&(clearTimeout(t.timer),t.timer=0),t.left=0;for(let e of t.nodes)try{e.stop(0)}catch{}try{t.gain.disconnect()}catch{}this.seVoices=this.seVoices.filter(e=>e!==t)}_out(){return this.ctx?(this._bus||(this._master=this.ctx.createGain(),this._master.gain.value=this._muted?0:this.volume,this._master.connect(this.ctx.destination),this._bus=this.ctx.createGain(),this._bus.gain.value=1,this._bus.connect(this._master),this._keepSec>0&&this._startTap(this._keepSec)),this._bus):null}mute(t){this._muted=t===void 0?!this._muted:!!t,this._out();let e=this._master;if(e){let s=this.ctx.currentTime;e.gain.cancelScheduledValues(s),e.gain.setValueAtTime(e.gain.value,s),e.gain.linearRampToValueAtTime(this._muted?0:this.volume,s+.05)}return this._muted}get volume(){return this._vol==null?1:this._vol}set volume(t){if(this._vol=Math.max(0,Math.min(8,Number(t)||0)),this._master&&!this._muted){let e=this.ctx.currentTime;this._master.gain.cancelScheduledValues(e),this._master.gain.setValueAtTime(this._master.gain.value,e),this._master.gain.linearRampToValueAtTime(this._vol,e+.05)}}get muted(){return!!this._muted}};var Dt={ア:["","a"],イ:["","i"],ウ:["","u"],エ:["","e"],オ:["","o"],カ:["k","a"],キ:["k","i"],ク:["k","u"],ケ:["k","e"],コ:["k","o"],ガ:["g","a"],ギ:["g","i"],グ:["g","u"],ゲ:["g","e"],ゴ:["g","o"],サ:["s","a"],シ:["sh","i"],ス:["s","u"],セ:["s","e"],ソ:["s","o"],ザ:["z","a"],ジ:["j","i"],ズ:["z","u"],ゼ:["z","e"],ゾ:["z","o"],タ:["t","a"],チ:["ch","i"],ツ:["ts","u"],テ:["t","e"],ト:["t","o"],ダ:["d","a"],ヂ:["j","i"],ヅ:["z","u"],デ:["d","e"],ド:["d","o"],ナ:["n","a"],ニ:["n","i"],ヌ:["n","u"],ネ:["n","e"],ノ:["n","o"],ハ:["h","a"],ヒ:["h","i"],フ:["f","u"],ヘ:["h","e"],ホ:["h","o"],バ:["b","a"],ビ:["b","i"],ブ:["b","u"],ベ:["b","e"],ボ:["b","o"],パ:["p","a"],ピ:["p","i"],プ:["p","u"],ペ:["p","e"],ポ:["p","o"],マ:["m","a"],ミ:["m","i"],ム:["m","u"],メ:["m","e"],モ:["m","o"],ヤ:["y","a"],ユ:["y","u"],ヨ:["y","o"],ラ:["r","a"],リ:["r","i"],ル:["r","u"],レ:["r","e"],ロ:["r","o"],ワ:["w","a"],ヲ:["","o"],ン:["","n"],ヮ:["w","a"],ヰ:["","i"],ヱ:["","e"],ヴ:["b","u"]},gt={ャ:"a",ュ:"u",ョ:"o",ァ:"a",ィ:"i",ゥ:"u",ェ:"e",ォ:"o"},Re="ャュョ";function Zt(n,t){if(!n.length)return;let e=n[n.length-1];Re.includes(t)&&e.v==="i"&&(e.pal=!0),e.v=gt[t]}var Be={0:"ゼロ",1:"イチ",2:"ニ",3:"サン",4:"ヨン",5:"ゴ",6:"ロク",7:"ナナ",8:"ハチ",9:"キュウ"},Ie={A:"エー",B:"ビー",C:"シー",D:"ディー",E:"イー",F:"エフ",G:"ジー",H:"エイチ",I:"アイ",J:"ジェー",K:"ケー",L:"エル",M:"エム",N:"エヌ",O:"オー",P:"ピー",Q:"キュー",R:"アール",S:"エス",T:"ティー",U:"ユー",V:"ブイ",W:"ダブリュー",X:"エックス",Y:"ワイ",Z:"ゼット"};function Oe(n){return n>="0"&&n<="9"||n>="A"&&n<="Z"||n>="a"&&n<="z"?String.fromCharCode(n.charCodeAt(0)-65248):n}function te(n){let t=Oe(n);return Be[t]||Ie[t.toUpperCase()]||null}function ee(n){return n.replace(/[ぁ-ゖ]/g,t=>String.fromCharCode(t.charCodeAt(0)+96))}function tt(n){let t=ee(String(n)),e=[],s=0;for(let r=0;r<t.length;r++){let i=t[r];if(Dt[i]||gt[i]||i==="ー"||i==="ッ"){s++;continue}let c=te(i);if(c){s+=[...c].filter(a=>!gt[a]&&a!=="ー").length;continue}if("  、,。!!??…〜~".includes(i)||"()()".includes(i))continue;if(i==="."&&t.slice(r,r+3)==="..."){r+=2;continue}if("><><".includes(i)||"'‘’'/+;^^__".includes(i)||i===`
3
+ `||i==="\r"||i===" ")continue;if(i==="@"){let a=t[r+1],u=r+2;for(;u<t.length&&t[u]>="0"&&t[u]<="9";)u++;if(u>r+2&&"psvPSV".includes(a)){r=u-1;continue}}let o=/[\u4e00-\u9fff\u3005]/.test(i)?"kanji":"other";e.push({ch:i,at:r,kind:o})}return{ok:e.length===0,kana:s,unknown:e}}function j(n){let t=ee(String(n)),e=[],s=!1,r=!1,i=0,c=!1;for(let o=0;o<t.length;o++){let a=t[o];if(a==="ー"){e.length&&(e[e.length-1].hold+=1);continue}if(a==="〜"||a==="~"){e.length&&(e[e.length-1].hold+=1,e[e.length-1].wave=!0);continue}if(a==="ッ"){s=!0;continue}if(a==="^"||a==="^"){r=!0;continue}if(a==="("||a==="("){i=1,c=!0;continue}if(a===")"||a===")"){i=0,c=!1;continue}if(a==="@"){let d=t[o+1],f=o+2,l="";for(;f<t.length&&t[f]>="0"&&t[f]<="9";)l+=t[f++];o=f-1;let p={p:"pitch",s:"speed",v:"vol"}[String(d).toLowerCase()];if(l&&p){let m=p==="speed"?+l/100:p==="vol"?Math.max(0,Math.min(15,+l))/15:+l;e.push({pause:1e-4,set:{[p]:m}})}continue}if(a===">"||a===">"){e.push({pause:1e-4,tune:1});continue}if(a==="<"||a==="<"){e.push({pause:1e-4,tune:-1});continue}if(gt[a]){Zt(e,a);continue}if(a===" "||a===" "||a==="_"||a==="_"){e.push({pause:.12});continue}if(a==="、"||a===","){e.push({pause:.12,brk:!0,mark:","});continue}if("。!!??…".includes(a)||a==="."&&t.slice(o,o+3)==="..."){let d=m=>m==="!"?"!":m==="?"?"?":m,f=d(a),l=d(t[o+1]||""),p;if(f==="!"&&l==="?"||f==="?"&&l==="!"?(p="!?",o++):f==="…"?p="…":f==="."?(p="…",o+=2):p=f==="!"?"!":f==="?"?"?":".",p==="…")for(;t[o+1]==="…";)o++;e.push({pause:p==="…"?.42:.24,brk:!0,mark:p});continue}let u=te(a);if(u){for(let d of u){if(d==="ー"){e.length&&(e[e.length-1].hold+=1);continue}if(d==="ッ"){s=!0;continue}if(gt[d]){Zt(e,d);continue}let f=Dt[d];e.push({c:f[0],v:f[1],hold:0,stop:s,...r?{devoice:!1}:null}),s=!1,r=!1}continue}let g=Dt[a];g&&(e.push({c:g[0],v:g[1],hold:0,stop:s,tie:i>1,lift:c,...r?{devoice:!1}:null}),i&&i++,s=!1,r=!1)}return e}var ne=4,se=-1,ie="、,;",Fe=.25,Ct="。??!!",ae=n=>String(n).replace(/[\u2018\u2019\uff07]/g,"'").replace(/\r\n?/g,`
4
+ `),re=(n,t=!0)=>n.replace(t?/['\/_\u301c\uff5e><\uff1e\uff1c]/g:/['\/]/g,"");function oe(n){return j(re(n)).filter(t=>!t.pause&&!t.set&&!t.tune).length}function De(n,t,e){let s=[],r=0,i=c=>{for(;e<c;)s.push({pause:1e-4,tune:1}),e++;for(;e>c;)s.push({pause:1e-4,tune:-1}),e--};for(let c of n){if(c.pause||c.set||c.tune){s.push(c);continue}r===0?i(t===1?ne:0):r===1?i(t===1?se:ne):t>1&&r===t&&i(se),s.push(c),r++}return{moras:s,tune:e}}function Ce(n,t,e=!0){let s=j(re(n,e)),r=n.indexOf("'");if(!e&&r<0){let u=[];for(;t>0;)u.push({pause:1e-4,tune:-1}),t--;for(;t<0;)u.push({pause:1e-4,tune:1}),t++;return u.push(...s),{moras:u,tune:0}}let i=r<0?0:oe(n.slice(0,r)),c=new Set;for(let u=0;u<n.length;u++)n[u]==="_"&&c.add(oe(n.slice(0,u)));let o=0,a=e?s.map(u=>u.pause||u.set||u.tune?u:{...u,devoice:c.has(o++)}):s;return De(a,i,t)}function qt(n,t={}){let e=t.strict!==!1,s=ae(n),r=[],i=0,c="",o=a=>{if(c.trim()){let u=Ce(c,i,e);r.push(...u.moras),i=u.tune}c="",a&&(Ct.includes(a)&&(i=0),a===";"?r.push({pause:Fe,brk:!0,mark:","}):r.push(...j(e&&Ct.includes(a)?"。":a)))};for(let a of s){if(a==="/"){o("");continue}if(a!=="+"){if(a===";"){o(";");continue}if(ie.includes(a)){o(a);continue}if(Ct.includes(a)){o(a==="?"?"?":a==="!"?"!":a);continue}if(a===" "||a===" "||a===`
5
+ `){o(" ");continue}c+=a}}return o(""),r}function wt(n){let t="";for(let e of ae(n)){if(e===`
6
+ `){t+=" ";continue}e==="/"||e==="_"||e==="'"||e==="+"||(t+=ie.includes(e)?"、":e)}return tt(t)}function le(n){let t=n&&n.talk;return t&&t.audioItems?(t.audioKeys||Object.keys(t.audioItems)).map(s=>t.audioItems[s]).filter(s=>s&&s.query).map(s=>s.query):n&&(n.accentPhrases||n.accent_phrases)?[n]:[]}var ce=n=>(+n.consonantLength||+n.consonant_length||0)+(+n.vowelLength||+n.vowel_length||0),lt=(n,t,e,s)=>{let r=n[t]??n[e];return r??s};function qe(n,t){let e=[];for(let s of n){let r=t&&+lt(s,"speedScale","speed_scale",1)||1,i=t&&+lt(s,"pitchScale","pitch_scale",0)||0,c=t?+lt(s,"intonationScale","intonation_scale",1):1,o=t&&+lt(s,"pauseLengthScale","pause_length_scale",1)||1,a=t&&+lt(s,"prePhonemeLength","pre_phoneme_length",0)||0,u=t&&+lt(s,"postPhonemeLength","post_phoneme_length",0)||0,g=s.accentPhrases||s.accent_phrases||[],d=[];for(let p of g)for(let m of p.moras||[])+m.pitch>0&&d.push(+m.pitch);let f=d.length?d.reduce((p,m)=>p+m,0)/d.length:0,l=c>=0?c:1;a>0&&e.push({gap:a/r});for(let p of g){let m=p.moras||[];for(let z of m){let w=+z.pitch||0;e.push({kana:String(z.text||""),pitch:w>0?f+(w-f)*l+i*Math.LN2:0,sec:ce(z)/r})}let N=p.pauseMora||p.pause_mora;N&&e.push({gap:ce(N)*o/r})}u>0&&e.push({gap:u/r}),e.push({end:!0})}return e}function Gt(n,t={}){let e=n;if(typeof e=="string")try{e=JSON.parse(e)}catch{e=null}let s=le(e);if(!s.length)return console.warn("[RetroTalk] VOICEVOX の読みが見つかりません(.vvproj の talk か、/audio_query の返しを渡してください)"),"";let r=t.usePitch!==!1,i=t.useLength!==!1,c=qe(s,t.useMeta!==!1),o=+t.speed>0?+t.speed:1,a=c.filter(m=>m.sec>0).map(m=>m.sec),u=a.length?a.reduce((m,N)=>m+N,0)/a.length:0,g=m=>Math.max(50,Math.min(800,Math.round(Math.exp(m)))),d=m=>Math.round(100*Math.max(.6,Math.min(1.8,o*Math.sqrt(u/m)))),f="",l=Math.round(o*100),p=0;for(let m of c){if(m.end){f+="。";continue}if(m.gap!==void 0){i?m.gap>=.2?f+="。":m.gap>=.08?f+="、":f+=" ":f+="、";continue}if(r&&m.pitch>0&&g(m.pitch)!==p&&(p=g(m.pitch),f+="@p"+p),i&&u>0&&m.sec>0){let N=d(m.sec);N!==l&&(l=N,f+="@s"+l)}f+=m.kana}return f.replace(/[、。 ]*。[、。 ]*/g,"。").replace(/[、 ]*、[、 ]*/g,"、").replace(/ +/g," ").replace(/^[、。 ]+/,"").trim()}function ue(n){let t=n;if(typeof t=="string")try{t=JSON.parse(t)}catch{t=null}let e=le(t),s=0;for(let r of e)for(let i of r.accentPhrases||r.accent_phrases||[])s+=(i.moras||[]).length;return{ok:e.length>0&&s>0,version:String(t&&(t.appVersion||t.app_version)||""),lines:e.length,moras:s}}function fe(n,t={}){return j(Gt(n,t))}var jt={c:0,d:2,e:4,f:5,g:7,a:9,b:11};function he(n){let t=String(n).toLowerCase(),e=[],s=[],r=4,i=4,c=120,o=0,a=1,u=1,g=(l,p)=>{l.includes(p)||l.push(p)},d=()=>{let l="";for(;o<t.length&&t[o]>="0"&&t[o]<="9";)l+=t[o++];return l===""?null:parseInt(l,10)},f=()=>{let l=d()??i,p=240/c/Math.max(1,l),m=p;for(;t[o]===".";)o++,m/=2,p+=m;return p};for(;o<t.length;){let l=t[o++];if(!`
7
+ \r|`.includes(l)){if(jt[l]!==void 0){let p=jt[l];for(;t[o]==="+"||t[o]==="#";)p++,o++;for(;t[o]==="-";)p--,o++;let m=f(),N=(r+1)*12+p;e.push({hz:440*Math.pow(2,(N-69)/12),sec:m,rest:!1,vol:a,q:u});continue}if(l==="r"){e.push({hz:0,sec:f(),rest:!0,vol:a,q:1});continue}if(l==="&"){for(;o<t.length&&`
8
+ \r`.includes(t[o]);)o++;if(jt[t[o]]!==void 0&&e.length){for(o++;"+#-".includes(t[o]);)o++;e[e.length-1].sec+=f()}continue}if(l==="o"){r=Math.max(1,Math.min(8,d()??r));continue}if(l===">"){r=Math.min(8,r+1);continue}if(l==="<"){r=Math.max(1,r-1);continue}if(l==="l"){i=d()??i;continue}if(l==="t"){c=Math.max(20,d()??c);continue}if(l==="@"){let p="";if(t[o]&&t[o]!=="{"&&(p=t[o++]),t[o]==="{"){for(;o<t.length&&t[o]!=="}";)o++;o++}else d();g(s,"@"+p);continue}if(l==="'"){for(;o<t.length&&t[o]!=="'";)o++;o++,f(),g(s,"'…'");continue}if(l==="v"){a=Math.max(0,Math.min(15,d()??15))/15;continue}if(l==="q"){u=Math.max(0,Math.min(8,d()??8))/8;continue}if("$p".includes(l)){d(),g(s,l);continue}if("[]".includes(l)){d(),g(s,l);continue}g(s,l)}}return{notes:e,warn:s}}var Lt=1006e-7,D=Math.round(1/Lt),Ge=.01,yt=1,Nt=128,Ae=136.2,Se=.0552,Wt=n=>Ae*Math.exp(Se*n),je=n=>Math.log(n/Ae)/Se,Pt=[62,0,57,47,32],Et=[1,4,3],Ut=[9,4,9,9,11],xe=16,$t=n=>40*Math.pow(30,n/(xe-1)),Ke=[3,4.5,6.5],He=[3,8,9],Ue=[3,13,14],We=[3,7,9],$e=[8,9,10],Ye=[12,13,13],Xe=15,Je=6,Qe=4,Ze=2,zt=["flat","natural","adsr","vibrato"],F={attack:6,attackDrop:5,decay:10,sustain:1,release:6,releaseDrop:4,vibWait:30,vibRamp:16,vibHz:4.5,vibCents:45},tn=3,en=.7,nn=.6;function sn(n,t,e,s){let r=Math.max(1,Math.round(e*(1-1/_e))),i=e-r,c=Math.round(i*rn);for(let o=0;o<e;o++){let a=o<=c?0:Math.min(1,(o-c)/Math.max(1,s)),u=t.amp+on*(1-Math.pow(a,.6));o>=i&&(u-=ln*((o-i+1)/r)),n.push({...t,amp:Math.max(1,u),frames:1})}}var on=3,an=5,rn=.5,_e=1.2,cn=1.4,ln=6;function un(n,t,e){for(let s=0;s<e;s++){let r=s/Math.max(1,e-1),i=tn*Math.pow(r,en)+nn*Math.max(0,1-s/2);n.push({...t,amp:Math.max(1,t.amp-i),frames:1})}}var Tt={hz:4,cents:60,ramp:5,sag:2};function fn(n,t,e,s,r){for(let i=0;i<e;i++){let c=Math.min(1,(i+1)/Tt.ramp),o=Tt.cents*c*Math.sin(2*Math.PI*Tt.hz*i*r),a=Math.max(yt,Math.min(Nt,Math.round(1/(s*Math.pow(2,o/1200)*Lt)))),u=Tt.sag*(i/Math.max(1,e-1));n.push({...t,P:a,amp:Math.max(1,Math.round(t.amp-u)),frames:1})}}function hn(n){let t=n-Math.floor(n);return t<.5?4*t-1:3-4*t}function pn(n,t,e,s,r){let{attackDrop:i,sustain:c,releaseDrop:o}=F,a=Math.max(1,Math.min(F.attack,Math.floor(e/3))),u=Math.max(1,Math.min(F.release,Math.floor(e/3))),g=Math.max(1,Math.min(F.decay,e-a-u));for(let d=0;d<e;d++){let f=0;d<a?f+=i*(1-(d+1)/a):d<a+g?f+=c*((d-a+1)/g):f+=c;let l=e-1-d;l<u&&(f+=o*(1-l/u));let p=t.P;if(r==="vibrato"&&d>=F.vibWait){let m=Math.min(1,(d-F.vibWait)/F.vibRamp),N=F.vibCents*m*hn(F.vibHz*(d-F.vibWait)*(F.frameSec||.01));p=Math.max(yt,Math.min(Nt,Math.round(1/(s*Math.pow(2,N/1200)*Lt))))}n.push({...t,P:p,amp:Math.max(1,Math.round(t.amp-f)),frames:1})}}var pe=.2,me=.94,mn=2,dn=.25,kn=9,gn=3,Mn=200,vn=7,An=2,Sn=2,xn=6,_n={palNasalFrames:xn,nasalAmp:null,nasalTail:!0,nasalizedGlide:!0,glideAfterNasal:"ramp",palF3:"vowel",voicedBurstAmp:12,voicedBurstVoice:!1,voicedGap:"voice",barAmp:vn,barBand:"bar"},de=new Set(["sh","ch","j","y"]),ke=n=>[ut.y.hz[0],ut.y.hz[1],(Mt[n]||Mt.a)[2]],ge=new Set(["k","s","t","h","p","sh","ch","ts","f"]),bn=8,wn=6,Kt=new Set(["!","!?"]),Tn=new Set(["!","?","!?"]);function En(n){let t=n.map(()=>!1),e=-1,s=[],r=()=>{!s.length||s.some(i=>!t[i])||(t[s[s.length-1]]=!1)};for(let i=0;i<n.length;i++){let c=n[i];if(c.set||c.tune)continue;if(c.pause){r(),s=[],e=-1;continue}let o=e;if(e=i,s.push(i),c.devoice!==void 0){t[i]=!!c.devoice;continue}if(c.v!=="i"&&c.v!=="u"||c.hold||!ge.has(c.c))continue;let a=i+1;for(;a<n.length&&(n[a].set||n[a].tune);)a++;let u=n[a];u&&!u.pause&&!ge.has(u.c)||u&&u.pause&&Tn.has(u.mark)||o>=0&&t[o]||(t[i]=!0)}return r(),t}var Mt={a:[648,1028,2560],i:[280,2230,3e3],u:[320,1300,2200],e:[470,1900,2550],o:[520,850,2500],n:[280,1200,2400]},Ht={m:[280,1e3,2200],n:[280,1500,2500],ng:[280,1900,2300]};function zn(n){return n==="m"||n==="p"||n==="b"?Ht.m:n==="k"||n==="g"?Ht.ng:n==="t"||n==="d"||n==="n"||n==="r"||n==="s"||n==="z"||n==="ts"||n==="ch"||n==="j"||n==="sh"?Ht.n:Mt.n}var ut={k:{kind:"burst",closure:6,frames:1,gap:3,hz:[300,1900,2300],hzBy:{i:[300,2900,3300],e:[300,2500,2900],u:[300,1300,2200],o:[300,1200,2300]}},g:{kind:"burst",closure:4,frames:1,gap:1,hz:[300,1900,2250],voiced:!0,hzBy:{i:[300,2700,3100],e:[300,2400,2800],u:[300,1300,2100],o:[300,1200,2200]}},t:{kind:"burst",closure:6,frames:1,gap:1,hz:[300,1750,3400]},d:{kind:"burst",closure:4,frames:1,gap:1,hz:[300,1700,3100],voiced:!0},p:{kind:"burst",closure:6,frames:1,gap:1,hz:[300,900,2300]},b:{kind:"burst",closure:4,frames:1,gap:1,hz:[300,900,2200],voiced:!0},s:{kind:"noise",frames:7,amp:11,hz:[500,3400,4e3]},z:{kind:"noise",frames:4,hz:[400,3e3,3600],voiced:!0},sh:{kind:"noise",frames:7,amp:11,hz:[500,2300,3100]},j:{kind:"noise",frames:4,hz:[400,2300,3e3],voiced:!0},ch:{kind:"noise",closure:4,frames:5,gap:1,hz:[500,2400,3400]},ts:{kind:"burst",closure:5,frames:3,gap:1,hz:[500,3300,3900]},h:{kind:"noise",frames:6,wide:!0,hz:[600,1500,2400],hzBy:{i:[300,2800,3300]}},f:{kind:"noise",frames:6,wide:!0,hz:[500,1600,2500]},n:{kind:"voiced",frames:4,amp:10,nasal:!0,hz:[280,1500,2500]},m:{kind:"voiced",frames:4,amp:10,nasal:!0,hz:[280,1e3,2300]},r:{kind:"voiced",frames:2,hz:[400,1400,1800]},y:{kind:"voiced",frames:3,hz:[300,2100,2900]},w:{kind:"voiced",frames:3,hz:[320,800,2200]}},Yt={pitch:280,pitchShift:0,tuneStep:1,volume:10,volumeShift:0,speedShift:1,speed:1,fall:4,rise:1,formantShift:mn,bandShift:0,breath:0,jitter:0,ring:0,interp:!1,frame:Ge,bits:0,style:"flat",slewF:Je,slewB:Qe,slewP:Ze,lag:0,lagPerMora:0,seed:1,gainNorm:!0,level:.15},S=Yt;function Ln(n){let t=n|0||2654435769;return()=>(t^=t<<13,t^=t>>>17,t^=t<<5,(t>>>0)/4294967296*2-1)}function K(n,t=0){let e=[t,t,t,t,t];return Et.forEach((s,r)=>{e[s]=je(n[r])-Pt[s]+t}),e}function $(n,t,e){let s=n?Ut.map(()=>Xe):Ut.slice(),r=n?t?Ye:$e:e==="nasal"?He:e==="bar"?Ue:e==="nasalized"?We:Ke;return Et.forEach((i,c)=>{s[i]=r[c]}),s}function be(n,t){let e=t.frame??S.frame,s={..._n,...t.assembly||{}},r=t.format!=="voicevox",i=r?t.fall??S.fall:0,c=r?t.rise??S.rise:0,o=[],a=Math.pow(2,(t.pitchShift??S.pitchShift)/12),u=(t.pitch??S.pitch)*a,g=Math.max(.05,t.speedShift??S.speedShift),d=t.tuneStep??S.tuneStep,f=(t.speed??S.speed)*g,l=(t.volumeShift??S.volumeShift)/15,p=Math.max(0,Math.min(15,t.volume??S.volume))/15,m=h=>Math.max(0,Math.min(1,h+l)),N=[],z=0,w=null,R=0,C=0,P=0,T=(h,M)=>{if(h.tie&&w){C--;let _=C>0?R:P;return C>0&&(P-=R),{...w,frames:_,gap:C>0?0:w.gap}}let v=V[At++];if(!v)return null;let E=1;for(let _=M+1;_<n.length;_++){let A=n[_];if(!(A.set||A.tune)){if(A.pause||!A.tie)break;E++}}return E<2?(w=null,v):(R=Math.max(1,Math.floor(v.frames/(E+1))),C=E-1,P=v.frames-R,w=v,{...v,frames:R,gap:0})},O=S.frame/e,L=h=>Math.max(1,Math.round(h*O/f)),et=h=>Math.max(1,Math.round(h/e/f)),H=2,B={".":{lift:0,up:c,down:i},"!":{lift:0,up:c,down:i},"?":{lift:0,up:c,down:i},"!?":{lift:0,up:c,down:i},"…":{lift:0,up:0,down:i*1.3}},nt={"!":()=>0,"?":()=>Math.max(2,i),"!?":()=>Math.max(2,i)},b={"!":[Math.max(1,i/3)],"?":[Math.max(2,i)],"!?":[Math.max(2,i)]},ft={"!":2,"!?":3,"…":-2},Y=0,st=12,ht=(h,M,v,E)=>{let _=b[M];if(_&&v&&v<=_.length)return u*Math.pow(2,(_[_.length-v]+Y)/12);let A=B[M]||B["."],x=A.lift+(E?h<.15?0:-A.down*((h-.15)/.85):h<.15?A.up*(h/.15):A.up-(A.up+A.down)*((h-.15)/.85));return u*Math.pow(2,(x+Y)/12)},V=t.notes||null;if(!V&&t.sing){let h=he(t.sing);h.warn.length&&console.warn("[RetroTalk] 歌の音符に読めないものがあります — "+h.warn.join(" ")+"(使えるのは cdefgab r + # - 数字 . l o > < t &)"),V=h.notes.map(M=>{let v=Math.max(1,Math.round(M.sec/e)),E=Math.max(1,Math.round(v*(M.q??1)));return{hz:M.hz,rest:M.rest,vol:M.vol,frames:E,gap:v-E}})}let q=t.formantShift??S.formantShift,U=t.style??S.style;zt.includes(U)||(console.warn(`[RetroTalk] 音の形に ${U} はありません(${zt.join(" / ")})`),U="flat"),F.frameSec=e;let At=0,St=[],Rt=[],xt=[];{let h=0,M=(v,E)=>{let _=Math.max(1,v-h-1);for(let x=h;x<v;x++)St[x]=(x-h)/_,Rt[x]=E;let A=0;for(let x=v-1;x>=h&&!(!(n[x].pause||n[x].set||n[x].tune)&&(xt[x]=++A,A>=2));x--);};for(let v=0;v<n.length;v++)n[v].brk&&(M(v,n[v].mark||"."),St[v]=0,Rt[v]=n[v].mark||".",h=v+1);M(n.length,".")}let Xt=[],Jt=[];for(let h=0;h<n.length;h++){if(!n[h].lift)continue;let M=h;for(;M+1<n.length&&n[M+1].lift;)M++;let v=null,E=null;for(let A=M+1;A<n.length;A++){let x=n[A];if(!(x.set||x.tune)){x.pause&&(E=x.mark,v=nt[x.mark]||null);break}}if(Kt.has(E))for(let A=h-1;A>=0;A--){let x=n[A];if(!(x.set||x.tune)){if(x.pause)break;Jt[A]=!0;break}}let _=[];for(let A=h;A<=M;A++)!n[A].pause&&!n[A].set&&_.push(A);for(let[A,x]of _.entries()){let Bt=_.length>1?A/(_.length-1):.5;Xt[x]={t:St[h]??0,up:v?v(Bt):0,mark:E}}h=M}let Pe=V?n.map(()=>!1):En(n),pt=0;for(let h of n){let M=pt++,v=St[M]??0;if(h.set){h.set.pitch>0&&(u=h.set.pitch*a),h.set.speed>0&&(f=h.set.speed*g),h.set.vol!==void 0&&(p=h.set.vol);continue}if(N[M]=m(p),h.tune){Y=Math.max(-st,Math.min(st,Y+h.tune*d));continue}if(h.pause){h.brk&&(Y=0),o.push({silent:!0,frames:et(h.pause),mora:M,brk:!!h.brk});continue}let E=Rt[pt-1]||".",_=Xt[M],A=_?E==="!"&&_.mark==="!":Kt.has(E)&&xt[pt-1]===1,x=A||_&&_.mark==="!?",Bt=Jt[M]||!_&&Kt.has(E)&&xt[pt-1]===2;if(z>0&&(o.push({silent:!0,frames:z,mora:M}),z=0),h.stop&&o.push({silent:!0,frames:L(6),mora:M}),!h.tie)for(;V&&V[At]&&V[At].rest;)o.push({silent:!0,frames:V[At++].frames,mora:M});let I=V?T(h,M):null;I&&I.gap>0&&(z=I.gap),I&&I.vol!==void 0&&(N[M]=m(p*I.vol));let It=I?I.hz:_?_.up?u*Math.pow(2,(_.up+Y)/12):ht(_.t,E,0,!0):ht(v,E,xt[pt-1]),Ve=E==="!"?0:ft[E]||0,X=Math.max(yt,Math.min(Nt,Math.round(1/(It*Lt)))),k=ut[h.c],G=(()=>{for(let y=M+1;y<n.length;y++){let Q=n[y];if(!(Q.set||Q.tune))return Q.pause?null:Q}return null})(),ot=h.v==="n"?zn(G&&G.c):Mt[h.v]||Mt.a,Ot=(()=>{if(!G)return null;let y=ut[G.c];return!y||y.kind==="noise"||G.c==="ts"?null:G.pal&&!de.has(G.c)&&(y.kind==="voiced"||y.voiced)?s.palF3==="y"?ut.y.hz:ke(G.v):y.hzBy&&y.hzBy[G.pal?"i":G.v]||y.hz})(),it=!!h.pal&&!!k&&!de.has(h.c),Qt=!!k&&(k.kind==="voiced"||!!k.voiced),mt=Math.min(15,(h.v==="n"?11:14)+Ve),at=Qt&&s.palF3!=="y"?ke(h.v):ut.y.hz;if(k){let y=it&&Qt&&!k.nasal?at:k.hzBy&&k.hzBy[it?"i":h.v]||k.hz,Q=k.kind!=="voiced",dt=k.voiced&&k.closure?[Mn,y[1],y[2]]:y;if(k.closure&&k.voiced?o.push({hz:dt,param:K(dt,q),bpos:$(!1,!1,s.barBand),P:X,amp:s.barAmp,voiced:!0,noise:!1,frames:L(k.closure),mora:M}):k.closure&&o.push({silent:!0,frames:L(k.closure),mora:M,glideTo:y,glideNoise:Q}),o.push({hz:dt,param:K(dt,q),bpos:$(Q,k.kind==="burst"||k.wide,k.nasal&&"nasal"),P:X,amp:k.nasal?s.nasalAmp??k.amp:k.voiced&&k.kind==="burst"?s.voicedBurstAmp:k.amp??(k.kind==="noise"?9:k.kind==="burst"?12:11),voiced:k.kind==="voiced"||!!k.voiced&&(k.kind==="noise"||s.voicedBurstVoice),noise:Q,fv:k.kind==="noise"&&!!k.voiced,frames:L(it&&k.nasal?s.palNasalFrames-(s.nasalTail?1:0):k.frames),mora:M}),it&&k.nasal&&s.nasalTail){let ct=[dt[0],at[1],at[2]];o.push({hz:ct,param:K(ct,q),bpos:$(!1,!1,"nasal"),P:X,amp:s.nasalAmp??k.amp??11,voiced:!0,noise:!1,frames:1,mora:M})}let kt=it?at:ot;if(k.gap&&k.voiced&&s.voicedGap==="silent")o.push({silent:!0,frames:L(k.gap),glideTo:kt,mora:M});else if(k.gap&&k.voiced)o.push({hz:kt,param:K(kt,q),bpos:$(!1),P:X,amp:Math.max(1,mt-2),voiced:!0,noise:!1,frames:L(k.gap),mora:M});else if(k.gap)for(let ct=0;ct<L(k.gap);ct++)o.push({hz:kt,param:K(kt,q),bpos:$(!0,!0),P:X,amp:Math.max(1,kn-gn*ct),voiced:!1,noise:!0,frames:1,mora:M})}let Ft=Pe[M],J=Ft?{hz:ot,param:K(ot,q),bpos:$(!0,!0),P:X,amp:bn,voiced:!1,noise:!0,mora:M}:{hz:ot,param:K(ot,q),bpos:$(!1),P:X,amp:mt,voiced:!0,noise:!1,mora:M},rt=it?L(Sn):0;if(rt){let y={hz:at,param:K(at,q),bpos:$(!1,!1,k.nasal&&s.nasalizedGlide&&"nasalized"),P:X,amp:Math.max(1,mt-2),voiced:!0,noise:!1,frames:rt,mora:M};k.nasal&&s.glideAfterNasal==="ramp"&&rt>1?(o.push({...y,frames:1}),o.push({...y,amp:mt,frames:rt-1})):k.nasal&&s.glideAfterNasal==="full"?o.push({...y,amp:mt}):o.push(y)}let Z=I?Math.max(1,I.frames-rt-(k?(k.closure||0)+k.frames+(k.gap||0):0)):Math.max(1,Math.round(L((Ft?wn:11)*(1+h.hold*.9))*(x?_e:Bt?cn:1))-rt),W=I||h.wave||Ft||!Ot?0:Math.min(An,Math.max(0,Z-3)),_t=()=>o.push({...J,param:K([ot[0],Ot[1],Ot[2]],q),amp:Math.max(1,J.amp-1),frames:W});h.wave&&!I?fn(o,J,Z,It,e):A&&!I?(sn(o,J,Z-W,L(an)),W&&_t()):U==="natural"?(un(o,J,Z-W),W&&_t()):U!=="flat"?(pn(o,J,Z-W,It,U),W&&_t()):W?(o.push({...J,frames:Z-W}),_t()):o.push({...J,frames:Z})}if(V){let h=V.filter(v=>!v.rest).length,M=n.filter(v=>!v.pause&&!v.set&&!v.tie).length;h!==M&&console.warn(`[RetroTalk] 歌: 拍 ${M} 個に対して音符 ${h} 個です`+(h<M?"(足りないぶんは、ふつうに読みます)":"(余ったぶんは鳴りません)"))}for(let h of o)h.vol=N[h.mora]??1;return o}function we(n,t){return t.format==="aquestalk"?qt(n):t.format==="voicevox"?fe(n,t):typeof n=="string"&&yn.test(n)?qt(n,{strict:!1}):j(n)}var yn=/['\u2018\u2019\uff07/+]/;function Te(n,t){let e=t.slewF??S.slewF,s=t.slewB??S.slewB,r=t.slewP??S.slewP,i=t.formantShift??S.formantShift,c=t.bandShift??S.bandShift,o=[],a=(f,l,p)=>f+Math.max(-p,Math.min(p,l-f)),u=[0,0,0,0,0],g=Ut.slice(),d=(n.find(f=>!f.silent&&f.P)||{}).P||30;for(let f=0;f<n.length;f++){let l=n[f];if(l.brk)for(let z=f+1;z<n.length;z++){let w=n[z];if(!(w.silent||!w.P)){d=w.P;break}}let p=l.silent?l.glideTo?K(l.glideTo,i):u:l.param,m=(l.silent?l.glideTo?$(!!l.glideNoise):g:l.bpos).map(z=>z+c),N=l.silent?d:l.P;for(let z=0;z<l.frames;z++)u=u.map((w,R)=>a(w,p[R],e)),g=g.map((w,R)=>Math.max(0,Math.min(xe-1,a(w,m[R],s)))),d=Math.max(yt,Math.min(Nt,a(d,N,r))),o.push({param:u.slice(),bpos:g.slice(),P:d,formant:Et.map(w=>Wt(u[w]+Pt[w])),formantBw:Et.map(w=>$t(g[w])),norm:Vn(u,g),amp:l.silent?0:l.amp,voiced:!l.silent&&l.voiced,noise:!l.silent&&l.noise,fv:!l.silent&&!!l.fv,mora:l.mora,vol:l.vol??1})}return o}function Ee(n,t={}){return Te(be(we(n,t),t),t)}function Me(n){let e=Math.abs(n);return e<=.9?n:Math.sign(n)*(.9+(1-.9)*Math.tanh((e-.9)/(1-.9)))}function ve(n,t){let e=0;for(let c=0;c<t;c++)e+=n[c]*n[c];let s=Math.sqrt(e/t)*.5,r=0,i=0;for(let c=0;c<t;c++)Math.abs(n[c])>s&&(r+=n[c]*n[c],i++);return i?Math.sqrt(r/i):0}var Nn=n=>n<=0?0:Math.pow(10,(n-15)*.125);function Pn(n,t,e){let s=Math.exp(-Math.PI*t/D),r=2*Math.PI*n/D,i=2*s*Math.cos(r),c=-s*s,o=1-i-c,a=2*Math.PI*e/D,u=1-i*Math.cos(a)-c*Math.cos(2*a),g=i*Math.sin(a)+c*Math.sin(2*a);return o/Math.sqrt(u*u+g*g)}function Vn(n,t){let e=n.map((i,c)=>Math.min(Wt(i+Pt[c]),D*.45)),s=t.map($t),r=0;for(let i of e){let c=1;for(let o=0;o<5;o++)c*=Pn(e[o],s[o],i);c>r&&(r=c)}return r>0?r:1}function Vt(n,t={}){let e=t.interp??S.interp,s=t.bits??S.bits,r=t.breath??S.breath,i=t.jitter??S.jitter,c=Math.max(0,t.ring??S.ring),o=Ln(t.seed??S.seed),a=we(n,t),u=be(a,t),g=Te(u,t),d=Math.max(0,(t.lag??S.lag)+(t.lagPerMora??S.lagPerMora)*a.length),f=Math.round((t.frame??S.frame)*D),l=Math.round(d*D),p=l+g.length*f+Math.round(.02*D),m=new Float32Array(p),N=new Float32Array(p).fill(1),z=new Float64Array(5),w=new Float64Array(5),R=0,C=l;for(let P=0;P<g.length;P++){let T=g[P],O=P>0?g[P-1]:T;for(let L=0;L<f;L++){let et=e||T.amp<=0||T.voiced!==O.voiced||T.noise!==O.noise?L/f:1,H=(b,ft)=>b+(ft-b)*et,B=0;if(T.noise&&(B=o()*pe),T.voiced){let b=Math.max(1,Math.round(H(O.P,T.P)));r>0&&(B+=o()*pe*r),i>0&&(b=Math.max(1,Math.round(b*(1+o()*i)))),--R<=0&&(B+=T.fv?dn:1,R=b)}else R=0;if(B*=Nn(H(O.amp,T.amp))/H(O.norm,T.norm),T.amp<=0)for(let b=0;b<5;b++)z[b]*=me,w[b]*=me;let nt=B;for(let b=0;b<5;b++){let ft=Math.min(Wt(H(O.param[b],T.param[b])+Pt[b]),D*.45),Y=$t(H(O.bpos[b],T.bpos[b])),st=Math.exp(-Math.PI*Y/D),ht=2*st*Math.cos(2*Math.PI*ft/D),V=-st*st,U=(1-ht-V)*nt+ht*z[b]+V*w[b];w[b]=z[b],z[b]=U,nt=U}c>0&&(nt*=Math.sin(2*Math.PI*c*(C/D))),C<p&&(N[C]=T.vol??1,m[C++]=nt)}}if(t.gainNorm??S.gainNorm){let P=Math.max(0,(t.formantShift??S.formantShift)-S.formantShift),T=(t.level??S.level)*(1+Math.min(.6,P*.1)),O=ve(m,p);if(O>0){for(let L=0;L<p;L++)m[L]=Me(m[L]*(T/O));for(let L=0;L<2;L++){let et=ve(m,p);if(!(et>0))break;let H=Math.min(1.5,T/et);if(H<=1.01)break;for(let B=0;B<p;B++)m[B]=Me(m[B]*H)}}}for(let P=0;P<p;P++)m[P]*=N[P];if(s>0){let P=Math.pow(2,s)/2;for(let T=0;T<p;T++)m[T]=Math.round(m[T]*P)/P}return{rate:D,data:m,frames:g.length}}var ze="\0talk:",vt=class extends bt{constructor(t,e={}){super(t,e),this.talkDefs=new Map,this.maxTalk=e.maxTalk??1,this._talkSeq=0}defineTalk(t,e,s={}){let r=s.unknown??"warn";if(r!=="ignore"){let i=s.format==="voicevox"?{ok:!0,unknown:[]}:s.format==="aquestalk"?wt(e):tt(e);if(!i.ok){let c=i.unknown.slice(0,8).map(g=>g.ch).join(" "),o=i.unknown.length>8?` ほか ${i.unknown.length-8} 字`:"",u=`[RetroTalk] talk "${t.startsWith(ze)?e:t}": 読めない字があります — ${c}${o}(漢字と数字は読みにしてから渡します)`;if(r==="error")throw new Error(u);console.warn(u)}}this.talkDefs.set(t,{text:e,opts:s,buffer:null})}talk(t,e={},s=0){let r=ze+ ++this._talkSeq;this.defineTalk(r,t,e);let i=this.playTalk(r,s,e);return this.talkDefs.delete(r),i}checkTalk(t){return tt(t)}prepareTalk(t){if(Array.isArray(t)){for(let c of t)this.prepareTalk(c);return}let e=this.talkDefs.get(t);if(!e||!this.ctx||e.buffer)return;let{rate:s,data:r}=Vt(e.text,e.opts),i=this.ctx.createBuffer(1,r.length,s);i.getChannelData(0).set(r),e.buffer=i}playTalk(t,e=0,s={}){if(this.unlock(),Array.isArray(t))return this.prepareTalk(t),t.map(f=>this.playTalk(f,e,s));let r=this.talkDefs.get(t);if(!r||!this.ctx)return;let i=Math.max(1,this.maxTalk|0)||1,c=this.seVoices.filter(f=>f.talk);for(let f=0;f<=c.length-i;f++)this._stopVoice(c[f]);let o=this.ctx.currentTime;if(this._cleanupSE(o),this.seVoices.some(f=>f.exclusive&&f.priority>e))return;for(;this._usedVoices()+1>this.maxVoices;){let f=null;for(let l of this.seVoices)l.priority>=e||(!f||l.priority<f.priority)&&(f=l);if(!f)return 0;this._stopVoice(f)}this.prepareTalk(t);let a=this.ctx.createGain();a.gain.value=(r.opts.gain??1)*.9,a.connect(this._out());let u=this.ctx.createBufferSource();u.buffer=r.buffer,u.connect(a);let g=o+.02;u.start(g);let d={gain:a,nodes:[u],priority:e,voices:1,noise:0,endTime:g+r.buffer.duration,exclusive:!!s.exclusive,talk:t,id:++this._seSeq};return this.seVoices.push(d),d.id}forgetTalk(t){if(t==null){this.talkDefs.clear();return}for(let e of Array.isArray(t)?t:[t])this.talkDefs.delete(e)}stopTalk(t){for(let e of[...this.seVoices])e.talk&&(t!==void 0&&e.talk!==t&&e.id!==t||this._stopVoice(e))}};function Le(n){let t=n.numberOfChannels,e=n.length,s=n.sampleRate,r=2,i=e*t*r,c=new Uint8Array(44+i),o=new DataView(c.buffer),a=0,u=l=>{for(let p of l)c[a++]=p.charCodeAt(0)},g=l=>{o.setUint32(a,l,!0),a+=4},d=l=>{o.setUint16(a,l,!0),a+=2};u("RIFF"),g(36+i),u("WAVE"),u("fmt "),g(16),d(1),d(t),g(s),g(s*t*r),d(t*r),d(8*r),u("data"),g(i);let f=[];for(let l=0;l<t;l++)f.push(n.getChannelData(l));for(let l=0;l<e;l++)for(let p=0;p<t;p++){let m=Math.max(-1,Math.min(1,f[p][l]));o.setInt16(a,Math.round(m<0?m*32768:m*32767),!0),a+=2}return c}var ye="2.1.0";Object.assign(vt,{renderTalk:Vt,talkFrames:Ee,parseTalk:j,checkTalk:tt,checkAqua:wt,voicevoxText:Gt,voicevoxInfo:ue,encodeWAV:Le,TALK_DEFAULTS:Yt,TALK_STYLES:zt,VERSION:ye});var Ne=vt;globalThis.MmsxxRetroTalk=Ne;})();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "retrotalk",
3
+ "version": "2.1.0",
4
+ "description": "Retro voice engine that sounds like a 1980s Japanese personal computer, back when they were called maikon",
5
+ "license": "MIT",
6
+ "author": "harayoki",
7
+ "homepage": "https://harayoki.github.io/retrotalk/",
8
+ "repository": { "type": "git", "url": "git+https://github.com/harayoki/retrotalk.git" },
9
+ "bugs": { "url": "https://github.com/harayoki/retrotalk/issues" },
10
+ "keywords": ["speech synthesis", "formant", "retro", "web audio", "japanese"],
11
+ "type": "module",
12
+ "exports": {
13
+ ".": "./src/index.js",
14
+ "./dist/retrotalk.js": "./dist/retrotalk.js",
15
+ "./dist/retrotalk.min.js": "./dist/retrotalk.min.js"
16
+ },
17
+ "main": "src/index.js",
18
+ "module": "src/index.js",
19
+ "browser": "dist/retrotalk.min.js",
20
+ "sideEffects": false,
21
+ "files": ["src", "dist", "LICENSE", "README.md"],
22
+ "scripts": {
23
+ "build": "node build.mjs",
24
+ "test": "node --test",
25
+ "prepublishOnly": "node build.mjs",
26
+ "docs": "jsdoc -c jsdoc.json && node -e \"const{globSync,rmSync}=require('fs');globSync('docs/api/**/*.md').forEach((f)=>rmSync(f))\""
27
+ },
28
+ "devDependencies": {
29
+ "clean-jsdoc-theme": "^5.2.0",
30
+ "esbuild": "^0.28.2",
31
+ "jsdoc": "^4.0.5"
32
+ },
33
+ "publishConfig": { "access": "public" }
34
+ }
@@ -0,0 +1,247 @@
1
+ // SPDX-FileCopyrightText: 2026 harayoki
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ // AquesTalk 風の記号列を読み取ります。
5
+ //
6
+ // カタカナの読みに、アクセントの位置と無声化する拍を記号で書き添えた形式です。
7
+ // この 2 つは辞書が無いと決められないので、書く人から記号で受け取ります。
8
+ // VOICEVOX の /audio_query が返す kana も同じ形式なので、そのまま渡せます。
9
+ //
10
+ // 記号の一覧
11
+ //
12
+ // | 記号 | 意味 |
13
+ // | --- | --- |
14
+ // | カタカナ | 読み。ひらがなでも動きます(内部で揃えます) |
15
+ // | `'` | アクセント核。その拍の直後で音が下がります。1 アクセント句に 1 つまで |
16
+ // | `/` | アクセント句の区切り。間は空きません |
17
+ // | `、` `,` | アクセント句の区切り。短い間が空きます |
18
+ // | `;` | アクセント句の区切り。長めの間が空きます |
19
+ // | `+` | 前の句に続けます。切らずに 1 つの句として読みます |
20
+ // | `_` | 無声化。次の 1 拍を息だけにします |
21
+ // | `。` `?` `!` | 文の終わり。間を置いて、音の高さを取り直します |
22
+ // | `@p` `@s` `@v` | RetroTalk の指示。高さ、速さ、音量はそのまま効きます |
23
+ //
24
+ // 高さを変える記号(`〜` `>` `<`)は読み飛ばします。`!` `?` による語尾の変化も
25
+ // 付けません。記号列はアクセントの情報を持っているので、両方を効かせると
26
+ // どちらの読み方になるのか決まらなくなります。
27
+ //
28
+ // 高さは東京式のアクセントに合わせて付けます。アクセント句の 1 拍目は低く、
29
+ // 2 拍目で上がり、核の位置まで高いまま、核の直後で下がります。
30
+ // 核が 1 拍目にあるときは上がらずに下がります。核が無いとき(平板)は上がったままです。
31
+ // 段は「低い」「高い」「下がったあと」の 3 つだけで、拍ごとに滑らかには動かしません。
32
+
33
+ import { parseTalk, checkTalk } from './kana.js';
34
+
35
+ /**
36
+ * アクセントで上がる幅(半音)。長三度あたり
37
+ * @private
38
+ */
39
+ const STEP = 4;
40
+ /**
41
+ * 核のあとで下がる先(半音)。始まりより少し低いところまで下げる
42
+ * @private
43
+ */
44
+ const AFTER = -1;
45
+
46
+ /**
47
+ * アクセント句の区切りのうち、間が空くもの(`/` は空かず、`+` は切らない)
48
+ * @private
49
+ */
50
+ const BREAKS = '、,;';
51
+ /**
52
+ * 長めの間(秒)。`、` は 0.12 なので、その倍を目安にする
53
+ * @private
54
+ */
55
+ const LONG_PAUSE = 0.25;
56
+ /**
57
+ * 文の終わり。読み方が変わるので、そのまま渡す
58
+ * @private
59
+ */
60
+ const ENDS = '。??!!';
61
+
62
+ /**
63
+ * 書きやすい形に均します。日本語入力で `'` を打つと `’` になることが多いので、
64
+ * どちらも同じ記号として受けます。改行は句の切れ目として読みます
65
+ * @private
66
+ */
67
+ const tidy = (s) => String(s).replace(/[\u2018\u2019\uff07]/g, "'").replace(/\r\n?/g, '\n');
68
+
69
+ /**
70
+ * 記号を取り除いて、読みだけにします。
71
+ * 高さを変える `〜` `>` `<` も取り除きます(記号列のアクセントとぶつかるため)
72
+ * @private
73
+ */
74
+ const kanaOnly = (s, strict = true) => s.replace(
75
+ strict ? /['\/_\u301c\uff5e><\uff1e\uff1c]/g : /['\/]/g, '');
76
+
77
+ /**
78
+ * 拍の数を数えます。記号は数に入りません。
79
+ * `parseTalk` に読ませて、間や上げ下げの指示を除いた数を返します
80
+ * @private
81
+ */
82
+ function moraCount(kana) {
83
+ return parseTalk(kanaOnly(kana)).filter((m) => !m.pause && !m.set && !m.tune).length;
84
+ }
85
+
86
+ /**
87
+ * アクセント句 1 つに、高さの上げ下げを付けます。
88
+ *
89
+ * 東京式のアクセントに合わせるために使います。1 拍目は低く、2 拍目で上がり、
90
+ * 核まで高いまま、核の直後で下がります。核が 1 拍目なら上がらずに下がり、
91
+ * 核が無ければ上がったままになります。
92
+ *
93
+ * ```js
94
+ * const got = accentPhrase(parseTalk('コンニチワ'), 3, 0);
95
+ * ```
96
+ *
97
+ * VOICEVOX の読みからも使います。拍の並びと核の位置という同じ形で来るためです。
98
+ *
99
+ * @param {Array} moras 拍の並び(指示が混ざっていてかまいません)
100
+ * @param {number} acc 核の位置(1 から。0 なら平板)
101
+ * @param {number} tune いま何半音ぶん上がっているか
102
+ * @returns {{moras:Array, tune:number}} 付け足した並びと、句を出たあとの上げ下げ
103
+ * @private
104
+ */
105
+ export function accentPhrase(moras, acc, tune) {
106
+ const out = [];
107
+ let n = 0; // いま何拍目か(指示は数えない)
108
+ /** そこまでに要る上げ下げを、差分で置く */
109
+ const to = (want) => {
110
+ while (tune < want) { out.push({ pause: 0.0001, tune: 1 }); tune++; }
111
+ while (tune > want) { out.push({ pause: 0.0001, tune: -1 }); tune--; }
112
+ };
113
+ for (const m of moras) {
114
+ if (m.pause || m.set || m.tune) { out.push(m); continue; }
115
+ // 1 拍目は低い。2 拍目から核までは高い。核の次から下がる
116
+ if (n === 0) to(acc === 1 ? STEP : 0);
117
+ else if (n === 1) to(acc === 1 ? AFTER : STEP);
118
+ else if (acc > 1 && n === acc) to(AFTER);
119
+ out.push(m);
120
+ n++;
121
+ }
122
+ return { moras: out, tune };
123
+ }
124
+
125
+ /**
126
+ * アクセント句 1 つを、拍の並びにします。
127
+ *
128
+ * @param {string} src 記号を含んだアクセント句
129
+ * @param {number} tune いま何半音ぶん上がっているか
130
+ * @returns {{moras:Array, tune:number}} 付け足した並びと、句を出たあとの上げ下げ
131
+ * @private
132
+ */
133
+ function phrase(src, tune, strict = true) {
134
+ const moras = parseTalk(kanaOnly(src, strict));
135
+ // 核の位置は、`'` の手前に何拍あるかで決まる
136
+ const at = src.indexOf("'");
137
+ // 核を書いていない句は、自動の抑揚に任せる(ふつうの文に混ぜたときだけ)。
138
+ // 核を書いた句だけを手で作りたいのに、`'` が 1 つでもあると
139
+ // 文じゅうが平らな型にはまっていた。
140
+ // 前の句で上げ下げが残っていたら、ここで 0 に戻してから渡す
141
+ if (!strict && at < 0) {
142
+ const out = [];
143
+ while (tune > 0) { out.push({ pause: 0.0001, tune: -1 }); tune--; }
144
+ while (tune < 0) { out.push({ pause: 0.0001, tune: 1 }); tune++; }
145
+ out.push(...moras);
146
+ return { moras: out, tune: 0 };
147
+ }
148
+ const acc = at < 0 ? 0 : moraCount(src.slice(0, at));
149
+ // 息だけにする拍。`_` の直後の 1 拍
150
+ const off = new Set();
151
+ for (let i = 0; i < src.length; i++) {
152
+ if (src[i] === '_') off.add(moraCount(src.slice(0, i)));
153
+ }
154
+
155
+ // 無声化はここで決めきり、自動の規則には渡さない。
156
+ // ふつうの文に混ぜて書くとき(strict: false)は規則のままにして、
157
+ // 残したいところだけ `^` で指定してもらう
158
+ let n = 0;
159
+ const marked = strict
160
+ ? moras.map((m) => (m.pause || m.set || m.tune ? m : { ...m, devoice: off.has(n++) }))
161
+ : moras;
162
+ return accentPhrase(marked, acc, tune);
163
+ }
164
+
165
+ /**
166
+ * 記号列を、拍の並びにします。
167
+ *
168
+ * アクセントと無声化を指定して読ませたいときに使います。返す形は `parseTalk()` と
169
+ * 同じなので、そのあとの扱いは変わりません。
170
+ *
171
+ * ```js
172
+ * const moras = parseAqua("コ'ンニチワ/イ_イ テンキ デス。");
173
+ * ```
174
+ *
175
+ * @param {string} text 記号列
176
+ * @param {object} [opts] `strict` を false にすると、ふつうの文に
177
+ * 記号を混ぜて書けます。高さを変える記号や語尾の変化もそのまま効きます
178
+ * @returns {Array} 拍の並び
179
+ * @private
180
+ */
181
+ export function parseAqua(text, opts = {}) {
182
+ // 記号列として渡されたときだけ、ほかの指示を無効にする。
183
+ // ふつうの文に `'` を混ぜて書けるようにしたので、
184
+ // そちらでは `〜` `>` `<` も `!` `?` も今までどおり効かせる
185
+ const strict = opts.strict !== false;
186
+ const src = tidy(text);
187
+ const out = [];
188
+ let tune = 0;
189
+ let buf = '';
190
+ /** 溜めた 1 句を並びにして、区切りを置く */
191
+ const flush = (sep) => {
192
+ if (buf.trim()) {
193
+ const got = phrase(buf, tune, strict);
194
+ out.push(...got.moras);
195
+ tune = got.tune;
196
+ }
197
+ buf = '';
198
+ if (sep) {
199
+ // 文の終わりで高さを戻す。戻さないと次の文が上がりっぱなしになる
200
+ if (ENDS.includes(sep)) tune = 0;
201
+ // `;` は長めの間。`、` と同じ切れ目にして、長さだけ伸ばす
202
+ if (sep === ';') out.push({ pause: LONG_PAUSE, brk: true, mark: ',' });
203
+ // 記号列では、文の終わりはどれも `。` として置く。語尾は核が決める。
204
+ // ふつうの文では書いたとおりに読む
205
+ else out.push(...parseTalk(strict && ENDS.includes(sep) ? '。' : sep));
206
+ }
207
+ };
208
+ for (const ch of src) {
209
+ if (ch === '/') { flush(''); continue; }
210
+ // `+` では切らない。前の句に続けて、1 つのアクセント句として読む
211
+ if (ch === '+') continue;
212
+ if (ch === ';') { flush(';'); continue; }
213
+ if (BREAKS.includes(ch)) { flush(ch); continue; }
214
+ if (ENDS.includes(ch)) { flush(ch === '?' ? '?' : ch === '!' ? '!' : ch); continue; }
215
+ if (ch === ' ' || ch === ' ' || ch === '\n') { flush(' '); continue; }
216
+ buf += ch;
217
+ }
218
+ flush('');
219
+ return out;
220
+ }
221
+
222
+ /**
223
+ * 記号列に読めない文字が無いかを調べます。読み上げはしません。
224
+ *
225
+ * 渡す前に確かめるときに使います。`checkTalk()` の記号列版で、返す形も同じです。
226
+ *
227
+ * ```js
228
+ * const got = checkAqua("コ'ンニチワ");
229
+ * // { ok: true, kana: 5, unknown: [] }
230
+ * ```
231
+ *
232
+ * @param {string} text 記号列
233
+ * @returns {{ok:boolean, kana:number, unknown:Array}}
234
+ * `ok` は読めない文字が無ければ true、`kana` は読めた文字数
235
+ * @alias TalkAudio.checkAqua
236
+ * @static
237
+ */
238
+ export function checkAqua(text) {
239
+ // 記号そのものは読める文字ではないので、取り除いてから調べる
240
+ let plain = '';
241
+ for (const ch of tidy(text)) {
242
+ if (ch === '\n') { plain += ' '; continue; }
243
+ if (ch === '/' || ch === '_' || ch === "'" || ch === '+') continue;
244
+ plain += BREAKS.includes(ch) ? '、' : ch;
245
+ }
246
+ return checkTalk(plain);
247
+ }
package/src/global.js ADDED
@@ -0,0 +1,10 @@
1
+ // SPDX-FileCopyrightText: 2026 harayoki
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ // `<script>` で読み込んだときに `MmsxxRetroTalk` を定義します。
5
+ //
6
+ // `index.js` は `import` して使う人のためのもので、名前を定義しません。
7
+
8
+ import RetroTalk from './index.js';
9
+
10
+ globalThis.MmsxxRetroTalk = RetroTalk;
package/src/index.js ADDED
@@ -0,0 +1,36 @@
1
+ // SPDX-FileCopyrightText: 2026 harayoki
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ // RetroTalk の入口です。
5
+ //
6
+ // クラス本体は `talk_audio.js` にあります。ここでは、再生を伴わない関数を
7
+ // そのクラスに付けて 1 つにまとめています。使う側が覚える名前を
8
+ // `MmsxxRetroTalk` の 1 つだけにするためです。
9
+
10
+ import { TalkAudio } from './talk_audio.js';
11
+ import { renderTalk, talkFrames, TALK_DEFAULTS, TALK_STYLES } from './talk.js';
12
+ import { checkTalk, parseTalk } from './kana.js';
13
+ import { checkAqua } from './aquestalk.js';
14
+ import { voicevoxText, voicevoxInfo } from './voicevox.js';
15
+ import { encodeWAV } from './wav.js';
16
+ import { RETROTALK_VERSION } from './version.js';
17
+
18
+ // 再生を伴わない関数は、クラスに付けます。インスタンスは声そのもので、
19
+ // こちらは声を作らずに調べたり変換したりする道具です。
20
+ // クラスに付けておくと、`new` しなくても使えることが呼び方から分かります。
21
+ Object.assign(TalkAudio, {
22
+ renderTalk,
23
+ talkFrames,
24
+ parseTalk,
25
+ checkTalk,
26
+ checkAqua,
27
+ voicevoxText,
28
+ voicevoxInfo,
29
+ encodeWAV,
30
+ TALK_DEFAULTS,
31
+ TALK_STYLES,
32
+ VERSION: RETROTALK_VERSION,
33
+ });
34
+
35
+ export { TalkAudio as MmsxxRetroTalk };
36
+ export default TalkAudio;