/** * 心经 / 金刚经 诵读页前端逻辑:经文渲染、分页阅读、逐段高亮朗读、音色选择。 */ // ===== 经文数据 ===== // segments 项:{ h: 品名(可选), g: 品名小注(可选), t: 正文 } const SUTRAS = [ { id: "xinjing", name: "心经", title: "般若波罗蜜多心经", seal: "心经", translator: "唐三藏法师 玄奘 译", source: "故宫博物院", sourceUrl: "https://www.dpm.org.cn/lemmas/243222.html", pageSize: 12, // 短篇一屏读完 segments: [ { t: "观自在菩萨,行深般若波罗蜜多时,照见五蕴皆空,度一切苦厄。" }, { t: "舍利子,色不异空,空不异色。色即是空,空即是色。受、想、行、识,亦复如是。" }, { t: "舍利子,是诸法空相,不生不灭,不垢不净,不增不减。" }, { t: "是故空中无色,无受、想、行、识;无眼、耳、鼻、舌、身、意;无色、声、香、味、触、法;" }, { t: "无眼界,乃至无意识界;" }, { t: "无无明,亦无无明尽,乃至无老死,亦无老死尽;" }, { t: "无苦、集、灭、道;无智亦无得。" }, { t: "以无所得故,菩提萨埵,依般若波罗蜜多故,心无挂碍。" }, { t: "无挂碍故,无有恐怖,远离颠倒梦想,究竟涅槃。" }, { t: "三世诸佛,依般若波罗蜜多故,得阿耨多罗三藐三菩提。" }, { t: "故知般若波罗蜜多,是大神咒,是大明咒,是无上咒,是无等等咒,能除一切苦,真实不虚。" }, { t: "故说般若波罗蜜多咒,即说咒曰:揭谛揭谛,波罗揭谛,波罗僧揭谛,菩提萨婆诃。" }, ], }, { id: "jingang", name: "金刚经", title: "金刚般若波罗蜜经", seal: "金刚经", translator: "姚秦三藏法师 鸠摩罗什 译", source: "维基文库 · 大正新修大藏经", sourceUrl: "https://zh.wikisource.org/wiki/金剛般若波羅蜜經_(鳩摩羅什)", pageSize: 4, // 每页 4 段 segments: [ { h: "法会因由分第一", g: "说法聚会,由此起因", t: "如是我闻。一时,佛在舍卫国祇树给孤独园,与大比丘众千二百五十人俱。尔时,世尊食时,著衣持钵,入舍卫大城乞食。于其城中,次第乞已,还至本处。饭食讫,收衣钵,洗足已,敷座而坐。" }, { h: "善现启请分第二", g: "善现长老,启请佛训", t: "时,长老须菩提,在大众中,即从座起,偏袒右肩,右膝著地,合掌恭敬,而白佛言:“希有世尊!如来善护念诸菩萨,善付嘱诸菩萨。世尊!善男子、善女人,发阿耨多罗三藐三菩提心,应云何住?云何降伏其心?”佛言:“善哉,善哉。须菩提!如汝所说:如来善护念诸菩萨,善付嘱诸菩萨。汝今谛听,当为汝说:善男子、善女人,发阿耨多罗三藐三菩提心,应如是住,如是降伏其心。”“唯然,世尊!愿乐欲闻。”" }, { h: "大乘正宗分第三", g: "最大之乘,最正之宗", t: "佛告须菩提:“诸菩萨摩诃萨,应如是降伏其心:所有一切众生之类,若卵生、若胎生、若湿生、若化生;若有色、若无色;若有想、若无想、若非有想非无想,我皆令入无馀涅槃而灭度之。如是灭度无量无数无边众生,实无众生得灭度者。何以故?须菩提!若菩萨有我相、人相、众生相、寿者相,即非菩萨。”" }, { h: "妙行无住分第四", g: "奥妙之行,本无住著", t: "“复次,须菩提!菩萨于法,应无所住,行于布施。所谓不住色布施,不住声、香、味、触、法布施。须菩提!菩萨应如是布施,不住于相。何以故?若菩萨不住相布施,其福德不可思量。”“须菩提!于意云何?东方虚空,可思量不?”“不也,世尊!”“须菩提!南、西、北方,四维上下虚空,可思量不?”“不也,世尊!”“须菩提!菩萨无住相布施,福德亦复如是不可思量。须菩提!菩萨但应如所教住。”" }, { h: "如理实见分第五", g: "自如之理,乃见真实", t: "“须菩提!于意云何?可以身相见如来不?”“不也,世尊!不可以身相得见如来。何以故?如来所说身相,即非身相。”佛告须菩提:“凡所有相,皆是虚妄。若见诸相非相,即见如来。”" }, { h: "正信希有分第六", g: "生正信心,最为希有", t: "须菩提白佛言:“世尊!颇有众生,得闻如是言说章句,生实信不?”佛告须菩提:“莫作是说。如来灭后,后五百岁,有持戒修福者,于此章句能生信心,以此为实,当知是人不于一佛、二佛、三四五佛而种善根,已于无量千万佛所种诸善根,闻是章句,乃至一念生净信者,须菩提!如来悉知悉见,是诸众生得如是无量福德。何以故?是诸众生无复我相、人相、众生相、寿者相。”“无法相,亦无非法相。何以故?是诸众生若心取相,则为著我、人、众生、寿者。若取法相,即著我、人、众生、寿者。何以故?若取非法相,即著我、人、众生、寿者。是故不应取法,不应取非法。以是义故,如来常说:汝等比丘,知我说法,如筏喻者,法尚应舍,何况非法。”" }, { h: "无得无说分第七", g: "空则无得,寂则无说", t: "“须菩提!于意云何?如来得阿耨多罗三藐三菩提耶?如来有所说法耶?”须菩提言:“如我解佛所说义,无有定法,名阿耨多罗三藐三菩提,亦无有定法,如来可说。何以故?如来所说法,皆不可取、不可说;非法,非非法。所以者何?一切贤圣,皆以无为法而有差别。”" }, { h: "依法出生分第八", g: "诸佛之法,依此生出", t: "“须菩提!于意云何?若人满三千大千世界七宝以用布施,是人所得福德,宁为多不?”须菩提言:“甚多,世尊!何以故?是福德即非福德性,是故如来说福德多。”“若复有人,于此经中受持,乃至四句偈等,为他人说,其福胜彼。何以故?须菩提!一切诸佛,及诸佛阿耨多罗三藐三菩提法,皆从此经出。须菩提!所谓佛法者,即非佛法,是名佛法。”" }, { h: "一相无相分第九", g: "只此一相,本来无形", t: "“须菩提!于意云何?须陀洹能作是念:我得须陀洹果不?”须菩提言:“不也,世尊!何以故?须陀洹名为入流,而无所入;不入色、声、香、味、触、法,是名须陀洹。”“须菩提!于意云何?斯陀含能作是念:我得斯陀含果不?”须菩提言:“不也,世尊!何以故?斯陀含名一往来,而实无往来,是名斯陀含。”“须菩提!于意云何?阿那含能作是念:我得阿那含果不?”须菩提言:“不也,世尊!何以故?阿那含名为不来,而实无不来,是故名阿那含。”“须菩提!于意云何?阿罗汉能作是念:我得阿罗汉道不?”须菩提言:“不也,世尊!何以故?实无有法名阿罗汉。世尊!若阿罗汉作是念:我得阿罗汉道,即为著我、人、众生、寿者。世尊!佛说我得无诤三昧,人中最为第一,是第一离欲阿罗汉。世尊!我不作是念:我是离欲阿罗汉。世尊!我若作是念:我得阿罗汉道,世尊则不说须菩提是乐阿兰那行者。以须菩提实无所行,而名须菩提,是乐阿兰那行。”" }, { h: "庄严净土分第十", g: "成就庄严,净明心地", t: "佛告须菩提:“于意云何?如来昔在燃灯佛所,于法有所得不?”“不也,世尊!如来在燃灯佛所,于法实无所得。”“须菩提!于意云何?菩萨庄严佛土不?”“不也,世尊!何以故?庄严佛土者,即非庄严,是名庄严。”“是故,须菩提!诸菩萨摩诃萨,应如是生清净心,不应住色生心,不应住声、香、味、触、法生心,应无所住而生其心。”“须菩提!譬如有人,身如须弥山王,于意云何?是身为大不?”须菩提言:“甚大,世尊!何以故?佛说非身,是名大身。”" }, { h: "无为福胜分第十一", g: "修无为福,胜于布施", t: "“须菩提!如恒河中所有沙数,如是沙等恒河,于意云何?是诸恒河沙,宁为多不?”须菩提言:“甚多,世尊!但诸恒河,尚多无数,何况其沙。”“须菩提!我今实言告汝:若有善男子、善女人,以七宝满尔所恒河沙数三千大千世界,以用布施,得福多不?”须菩提言:“甚多,世尊!”佛告须菩提:“若善男子、善女人,于此经中,乃至受持四句偈等,为他人说,而此福德,胜前福德。”" }, { h: "尊重正教分第十二", g: "受持正教,天人尊重", t: "“复次,须菩提!随说是经,乃至四句偈等,当知此处,一切世间,天、人、阿修罗,皆应供养,如佛塔庙。何况有人,尽能受持读诵。须菩提!当知是人成就最上第一希有之法,若是经典所在之处,即为有佛,若尊重弟子。”" }, { h: "如法受持分第十三", g: "当如此法,承受奉持", t: "尔时,须菩提白佛言:“世尊!当何名此经?我等云何奉持?”佛告须菩提:“是经名为金刚般若波罗蜜,以是名字,汝当奉持。所以者何?须菩提!佛说般若波罗蜜,即非般若波罗蜜,是名般若波罗蜜。须菩提!于意云何?如来有所说法不?”须菩提白佛言:“世尊!如来无所说。”“须菩提!于意云何?三千大千世界所有微尘,是为多不?”须菩提言:“甚多,世尊!”“须菩提!诸微尘,如来说非微尘,是名微尘。如来说世界,非世界,是名世界。须菩提!于意云何?可以三十二相见如来不?”“不也,世尊!不可以三十二相得见如来。何以故?如来说三十二相,即是非相,是名三十二相。”“须菩提!若有善男子、善女人,以恒河沙等身命布施;若复有人,于此经中,乃至受持四句偈等,为他人说,其福甚多!”" }, { h: "离相寂灭分第十四", g: "离诸形相,自得寂灭", t: "尔时,须菩提闻说是经,深解义趣,涕泪悲泣,而白佛言:“希有世尊!佛说如是甚深经典,我从昔来所得慧眼,未曾得闻如是之经。世尊!若复有人,得闻是经,信心清净,则生实相,当知是人,成就第一希有功德。世尊!是实相者,则是非相,是故如来说名实相。世尊!我今得闻如是经典,信解受持,不足为难;若当来世,后五百岁,其有众生,得闻是经,信解受持,是人则为第一希有。何以故?此人无我相、无人相、无众生相、无寿者相。所以者何?我相,即是非相;人相、众生相、寿者相,即是非相。何以故?离一切诸相,则名诸佛。”" }, { t: "佛告须菩提:“如是,如是!若复有人,得闻是经,不惊、不怖、不畏,当知是人,甚为希有。何以故?须菩提!如来说:第一波罗蜜,即非第一波罗蜜,是名第一波罗蜜。须菩提!忍辱波罗蜜,如来说非忍辱波罗蜜,是名忍辱波罗蜜。何以故?须菩提!如我昔为歌利王割截身体,我于尔时,无我相、无人相、无众生相、无寿者相。何以故?我于往昔,节节支解时,若有我相、人相、众生相、寿者相,应生瞋恨。须菩提!又念过去,于五百世,作忍辱仙人,于尔所世,无我相、无人相、无众生相、无寿者相。是故须菩提!菩萨应离一切相,发阿耨多罗三藐三菩提心。不应住色生心,不应住声、香、味、触、法生心,应生无所住心。若心有住,则为非住。”" }, { t: "“是故佛说:菩萨心不应住色布施。须菩提!菩萨为利益一切众生故,应如是布施。如来说:一切诸相,即是非相。又说:一切众生,即非众生。须菩提!如来是真语者、实语者、如语者、不诳语者、不异语者。”“须菩提!如来所得法,此法无实无虚。须菩提!若菩萨心住于法而行布施,如人入暗,则无所见;若菩萨心不住法而行布施,如人有目,日光明照,见种种色。”“须菩提!当来之世,若有善男子、善女人,能于此经,受持读诵,则为如来,以佛智慧,悉知是人,悉见是人,皆得成就无量无边功德。”" }, { h: "持经功德分第十五", g: "受持此经,功德无量", t: "“须菩提!若有善男子、善女人,初日分以恒河沙等身布施,中日分复以恒河沙等身布施,后日分亦以恒河沙等身布施,如是无量百千万亿劫,以身布施;若复有人,闻此经典,信心不逆,其福胜彼,何况书写、受持、读诵、为人解说。”“须菩提!以要言之,是经有不可思议、不可称量、无边功德。如来为发大乘者说,为发最上乘者说。若有人能受持读诵,广为人说,如来悉知是人,悉见是人,皆得成就不可量、不可称、无有边、不可思议功德。”“如是人等,则为荷担如来阿耨多罗三藐三菩提。何以故?须菩提!若乐小法者,著我见、人见、众生见、寿者见,则于此经不能听受、读诵,为人解说。”“须菩提!在在处处,若有此经,一切世间,天、人、阿修罗,所应供养;当知此处,则为是塔,皆应恭敬,作礼围绕,以诸华香而散其处。”" }, { h: "能净业障分第十六", g: "若能清净,业障尽消", t: "“复次,须菩提!善男子、善女人,受持读诵此经,若为人轻贱,是人先世罪业,应堕恶道,以今世人轻贱故,先世罪业则为消灭,当得阿耨多罗三藐三菩提。”“须菩提!我念过去无量阿僧祇劫,于燃灯佛前,得值八百四千万亿那由他诸佛,悉皆供养承事,无空过者;若复有人,于后末世,能受持读诵此经,所得功德,于我所供养诸佛功德,百分不及一,千万亿分、乃至算数譬喻所不能及。”“须菩提!若善男子、善女人,于后末世,有受持读诵此经,所得功德,我若具说者,或有人闻,心则狂乱,狐疑不信。须菩提!当知是经义不可思议,果报亦不可思议!”" }, { h: "究竟无我分第十七", g: "成佛究竟,本无我相", t: "尔时,须菩提白佛言:“世尊!善男子、善女人,发阿耨多罗三藐三菩提心,云何应住?云何降伏其心?”佛告须菩提:“善男子、善女人,发阿耨多罗三藐三菩提心者,当生如是心:我应灭度一切众生。灭度一切众生已,而无有一众生实灭度者。何以故?须菩提!若菩萨有我相、人相、众生相、寿者相,则非菩萨。所以者何?须菩提!实无有法,发阿耨多罗三藐三菩提心者。”“须菩提!于意云何?如来于燃灯佛所,有法得阿耨多罗三藐三菩提不?”“不也,世尊!如我解佛所说义,佛于燃灯佛所,无有法得阿耨多罗三藐三菩提。”" }, { t: "佛言:“如是,如是!须菩提!实无有法,如来得阿耨多罗三藐三菩提。须菩提!若有法,如来得阿耨多罗三藐三菩提者,燃灯佛则不与我授记:‘汝于来世,当得作佛,号释迦牟尼。’以实无有法得阿耨多罗三藐三菩提,是故燃灯佛与我授记,作是言:‘汝于来世,当得作佛,号释迦牟尼。’何以故?如来者,即诸法如义。”“若有人言:如来得阿耨多罗三藐三菩提。须菩提!实无有法,佛得阿耨多罗三藐三菩提。须菩提!如来所得阿耨多罗三藐三菩提,于是中无实无虚。是故如来说:一切法皆是佛法。须菩提!所言一切法者,即非一切法,是故名一切法。”" }, { t: "“须菩提!譬如人身长大。”须菩提言:“世尊!如来说:人身长大,则为非大身,是名大身。”“须菩提!菩萨亦如是。若作是言:我当灭度无量众生,则不名菩萨。何以故?须菩提!实无有法名为菩萨。是故佛说一切法,无我、无人、无众生、无寿者。”“须菩提!若菩萨作是言:‘我当庄严佛土’,是不名菩萨。何以故?如来说:庄严佛土者,即非庄严,是名庄严。”“须菩提!若菩萨通达无我、法者,如来说名真是菩萨。”" }, { h: "一体同观分第十八", g: "万法归一,更无异观", t: "“须菩提!于意云何?如来有肉眼不?”“如是,世尊!如来有肉眼。”“须菩提!于意云何?如来有天眼不?”“如是,世尊!如来有天眼。”“须菩提!于意云何?如来有慧眼不?”“如是,世尊!如来有慧眼。”“须菩提!于意云何?如来有法眼不?”“如是,世尊!如来有法眼。”“须菩提!于意云何?如来有佛眼不?”“如是,世尊!如来有佛眼。”“须菩提!于意云何?如恒河中所有沙,佛说是沙不?”“如是,世尊!如来说是沙。”“须菩提!于意云何?如一恒河中所有沙,有如是沙等恒河,是诸恒河所有沙数,佛世界如是,宁为多不?”“甚多,世尊。”佛告须菩提:“尔所国土中,所有众生,若干种心,如来悉知。何以故?如来说:诸心皆为非心,是名为心。所以者何?须菩提!过去心不可得,现在心不可得,未来心不可得。”" }, { h: "法界通化分第十九", g: "法身遍界,通化无边", t: "“须菩提!于意云何?若有人满三千大千世界七宝,以用布施,是人以是因缘,得福多不?”“如是,世尊!此人以是因缘,得福甚多。”“须菩提!若福德有实,如来不说得福德多;以福德无故,如来说得福德多。”" }, { h: "离色离相分第二十", g: "色相皆妄,离妄见性", t: "“须菩提!于意云何?佛可以具足色身见不?”“不也,世尊!如来不应以具足色身见。何以故?如来说具足色身,即非具足色身,是名具足色身。”“须菩提!于意云何?如来可以具足诸相见不?”“不也,世尊!如来不应以具足诸相见。何以故?如来说诸相具足,即非具足,是名诸相具足。”" }, { h: "非说所说分第二十一", g: "法无可说,所说非法", t: "“须菩提!汝勿谓如来作是念:我当有所说法。莫作是念。何以故?若人言如来有所说法,即为谤佛,不能解我所说故。须菩提!说法者,无法可说,是名说法。”尔时,慧命须菩提白佛言:“世尊!颇有众生,于未来世,闻说是法,生信心不?”佛言:“须菩提!彼非众生,非不众生。何以故?须菩提!众生众生者,如来说非众生,是名众生。”" }, { h: "无法可得分第二十二", g: "悟性空故,无法可得", t: "须菩提白佛言:“世尊!佛得阿耨多罗三藐三菩提,为无所得耶?”佛言:“如是,如是。须菩提!我于阿耨多罗三藐三菩提乃至无有少法可得,是名阿耨多罗三藐三菩提。”" }, { h: "净心行善分第二十三", g: "以清净心,行诸善法", t: "“复次,须菩提!是法平等,无有高下,是名阿耨多罗三藐三菩提;以无我、无人、无众生、无寿者,修一切善法,则得阿耨多罗三藐三菩提。须菩提!所言善法者,如来说即非善法,是名善法。”" }, { h: "福智无比分第二十四", g: "福智甚大,无物可比", t: "“须菩提!若三千大千世界中,所有诸须弥山王,如是等七宝聚,有人持用布施;若人以此般若波罗蜜经,乃至四句偈等,受持读诵,为他人说,于前福德百分不及一,百千万亿分、乃至算数譬喻所不能及。”" }, { h: "化无所化分第二十五", g: "圣凡同性,化无所化", t: "“须菩提!于意云何?汝等勿谓如来作是念:我当度众生。须菩提!莫作是念。何以故?实无有众生如来度者。若有众生如来度者,如来则有我、人、众生、寿者。须菩提!如来说有我者,则非有我,而凡夫之人以为有我。须菩提!凡夫者,如来说则非凡夫,是名凡夫。”" }, { h: "法身非相分第二十六", g: "清净法身,非属相貌", t: "“须菩提!于意云何?可以三十二相观如来不?”须菩提言:“如是!如是!以三十二相观如来。”佛言:“须菩提!若以三十二相观如来者,转轮圣王则是如来。”须菩提白佛言:“世尊!如我解佛所说义,不应以三十二相观如来。”尔时,世尊而说偈言:“若以色见我,以音声求我,是人行邪道,不能见如来。”" }, { h: "无断无灭分第二十七", g: "依法修持,不应断灭", t: "“须菩提!汝若作是念:如来不以具足相故,得阿耨多罗三藐三菩提。须菩提!莫作是念:如来不以具足相故,得阿耨多罗三藐三菩提。”“须菩提!汝若作是念,发阿耨多罗三藐三菩提心者,说诸法断灭。莫作是念!何以故?发阿耨多罗三藐三菩提心者,于法不说断灭相。”" }, { h: "不受不贪分第二十八", g: "一尘不染,何贪何受", t: "“须菩提!若菩萨以满恒河沙等世界七宝持用布施;若复有人,知一切法无我,得成于忍,此菩萨胜前菩萨所得功德。何以故?须菩提!以诸菩萨不受福德故。”须菩提白佛言:“世尊!云何菩萨不受福德?”“须菩提!菩萨所作福德,不应贪著,是故说不受福德。”" }, { h: "威仪寂静分第二十九", g: "真性寂静,不假威仪", t: "“须菩提!若有人言:如来若来若去,若坐若卧,是人不解我所说义。何以故?如来者,无所从来,亦无所去,故名如来。”" }, { h: "一合理相分第三十", g: "一合之理,实无有相", t: "“须菩提!若善男子、善女人,以三千大千世界碎为微尘,于意云何?是微尘众宁为多不?”须菩提言:“甚多,世尊!何以故?若是微尘众实有者,佛则不说是微尘众。所以者何?佛说微尘众,则非微尘众,是名微尘众。”“世尊!如来所说三千大千世界,则非世界,是名世界。何以故?若世界实有者,则是一合相。如来说一合相,则非一合相,是名一合相。”“须菩提!一合相者,则是不可说;但凡夫之人,贪著其事。”" }, { h: "知见不生分第三十一", g: "如此知见,法相不生", t: "“须菩提!若人言:佛说我见、人见、众生见、寿者见。须菩提!于意云何?是人解我所说义不?”“不也,世尊!是人不解如来所说义。何以故?世尊说我见、人见、众生见、寿者见,即非我见、人见、众生见、寿者见,是名我见、人见、众生见、寿者见。”“须菩提!发阿耨多罗三藐三菩提心者,于一切法,应如是知,如是见,如是信解,不生法相。须菩提!所言法相者,如来说即非法相,是名法相。”" }, { h: "应化非真分第三十二", g: "应现设化,亦非真实", t: "“须菩提!若有人以满无量阿僧祇世界七宝,持用布施;若有善男子、善女人,发菩提心者,持于此经,乃至四句偈等,受持读诵,为人演说,其福胜彼。云何为人演说?不取于相,如如不动。何以故?一切有为法,如梦幻泡影,如露亦如电,应作如是观。”佛说是经已,长老须菩提及诸比丘、比丘尼、优婆塞、优婆夷、一切世间天、人、阿修罗,闻佛所说,皆大欢喜,信受奉行。" }, ], }, ]; // ===== 可选音色(模型 qwen3-tts-flash) ===== const VOICE_GROUPS = [ { label: "诵读音色", voices: [ ["Seren", "Seren 塞伦 · 温润女声"], ["Ethan", "Ethan 伊森 · 阳光男声"], ], }, ]; const sutraEl = document.getElementById("sutra"); const voiceSelect = document.getElementById("voice-select"); const btnPlay = document.getElementById("btn-play"); const statusEl = document.getElementById("status"); const progressEl = document.getElementById("progress"); const audioEl = document.getElementById("audio"); const btnLayout = document.getElementById("btn-layout"); const pagerEl = document.getElementById("pager"); const playerCard = document.getElementById("player-card"); const btnExpand = document.getElementById("btn-expand"); const btnPrev = document.getElementById("btn-prev"); const btnNext = document.getElementById("btn-next"); const btnLoop = document.getElementById("btn-loop"); const btnBgm = document.getElementById("btn-bgm"); const pageInfo = document.getElementById("page-info"); /** 音频缓存:key = `${经id}|${音色}|${段序号}`,value = Blob */ const audioCache = new Map(); let playToken = 0; // 播放会话令牌,防止旧异步任务串扰 let playing = false; let inflight = null; // 进行中的 fetch AbortController let loopMode = false; // 循环诵读开关:播完全文回到开头继续 let bgmOn = false; // 背景音乐开关 let segTimes = []; // 合并音频中各段的起始秒 let currentSeg = -1; // 正在朗读的段号 // ===== 背景音乐:Web Audio 实时合成颂钵泛音 drone(零资源文件、无缝循环) ===== let bgmCtx = null; let bgmGain = null; let bgmNodes = []; let bgmBellTimer = null; let bgmDrumTimer = null; let bgmChimeTimer = null; let bgmGlideTimer = null; let bgmOscs = []; // {osc, ratio},供基频滑变统一改频率 const BGM_ROOTS = [55, 65.41, 73.42, 82.41, 98]; // A1 C2 D2 E2 G2,低音区垫底 let bgmRoot = 73.42; /** 基频慢移:只滑向相邻根音、16 秒缓慢过渡,变化若有似无,避免警报式的滑音。 */ function bgmGlide() { const idx = BGM_ROOTS.indexOf(bgmRoot); const neighbors = [idx - 1, idx + 1].filter((i) => i >= 0 && i < BGM_ROOTS.length); bgmRoot = BGM_ROOTS[neighbors[Math.floor(Math.random() * neighbors.length)]]; const t = bgmCtx.currentTime; for (const { osc, ratio } of bgmOscs) { osc.frequency.cancelScheduledValues(t); osc.frequency.setValueAtTime(osc.frequency.value, t); osc.frequency.exponentialRampToValueAtTime(bgmRoot * ratio, t + 16); } bgmGlideTimer = setTimeout(() => { if (bgmGain) bgmGlide(); }, 50000 + Math.random() * 40000); } /** 给打击声部接随机方位的输出链,返回挂到主增益下的 GainNode。 */ function bgmStrikeOut(panRange) { const out = bgmCtx.createGain(); if (bgmCtx.createStereoPanner) { const pan = bgmCtx.createStereoPanner(); pan.pan.value = (Math.random() - 0.5) * 2 * panRange; out.connect(pan); pan.connect(bgmGain); } else { out.connect(bgmGain); } return out; } /** 晨钟一声:大钟的非谐泛音簇,毫秒级起音,泛音越高衰减越快(长达十余秒)。 */ function bgmBell() { const now = bgmCtx.currentTime; const root = [98, 110, 123.47][Math.floor(Math.random() * 3)]; // G2 / A2 / B2 附近的大钟 const bellGain = bgmStrikeOut(0.8); bellGain.gain.setValueAtTime(0, now); bellGain.gain.linearRampToValueAtTime(0.5, now + 0.02); // 大钟泛音列:哼音 0.5、基音 1、小三度 1.19、五度 1.5、名义音 2、上泛音 2.74 // [频率比, 振幅, 衰减秒数] —— 低频泛音余韵更长 [[0.5, 0.5, 15], [1, 1, 13], [1.19, 0.3, 9], [1.5, 0.24, 7], [2.0, 0.18, 5], [2.74, 0.1, 3.5]].forEach(([ratio, amp, dur]) => { const o = bgmCtx.createOscillator(); o.type = "sine"; o.frequency.value = root * ratio; o.detune.value = (Math.random() - 0.5) * 5; const og = bgmCtx.createGain(); og.gain.setValueAtTime(amp, now); og.gain.exponentialRampToValueAtTime(0.0001, now + dur); o.connect(og); og.connect(bellGain); o.start(now); o.stop(now + dur + 0.5); bgmNodes.push(o); }); } /** 磬声一声:高音引磬,快起音 + 约 6 秒衰减的小泛音簇,稀疏点缀。 */ function bgmChime() { const now = bgmCtx.currentTime; const freq = [392, 523.25, 659.25, 783.99][Math.floor(Math.random() * 4)]; // 五声音阶附近 const chimeGain = bgmStrikeOut(0.6); chimeGain.gain.setValueAtTime(0, now); chimeGain.gain.linearRampToValueAtTime(0.35, now + 0.03); chimeGain.gain.exponentialRampToValueAtTime(0.0001, now + 6); [[1, 1], [2.76, 0.28], [5.4, 0.08]].forEach(([ratio, amp]) => { const o = bgmCtx.createOscillator(); o.type = "sine"; o.frequency.value = freq * ratio; o.detune.value = (Math.random() - 0.5) * 6; const og = bgmCtx.createGain(); og.gain.value = amp; o.connect(og); og.connect(chimeGain); o.start(now); o.stop(now + 6.5); bgmNodes.push(o); }); } /** 暮鼓一记:低频膜鸣(72→44Hz 下滑)+ 短促噪声击边,钝而沉。 */ function bgmDrum() { const now = bgmCtx.currentTime; const drumGain = bgmStrikeOut(0.5); drumGain.gain.setValueAtTime(0, now); drumGain.gain.linearRampToValueAtTime(0.55, now + 0.012); drumGain.gain.exponentialRampToValueAtTime(0.0001, now + 1.8); // 鼓皮基音:起振约 72Hz,迅速下滑到 44Hz const o = bgmCtx.createOscillator(); o.type = "sine"; o.frequency.setValueAtTime(72, now); o.frequency.exponentialRampToValueAtTime(44, now + 0.3); o.connect(drumGain); o.start(now); o.stop(now + 2); bgmNodes.push(o); // 击边噪声:0.08s 噪声过 260Hz 带通,模拟鼓槌触皮的一声"扑" const len = Math.floor(bgmCtx.sampleRate * 0.08); const buf = bgmCtx.createBuffer(1, len, bgmCtx.sampleRate); const data = buf.getChannelData(0); for (let i = 0; i < len; i += 1) data[i] = Math.random() * 2 - 1; const noise = bgmCtx.createBufferSource(); noise.buffer = buf; const bp = bgmCtx.createBiquadFilter(); bp.type = "bandpass"; bp.frequency.value = 260; bp.Q.value = 0.8; const ng = bgmCtx.createGain(); ng.gain.setValueAtTime(0.4, now); ng.gain.exponentialRampToValueAtTime(0.0001, now + 0.15); noise.connect(bp); bp.connect(ng); ng.connect(drumGain); noise.start(now); bgmNodes.push(noise); } /** * 开始播放背景音乐:低频颂钵式泛音 drone 垫底(基频缓慢游走), * 叠稀疏的晨钟(大钟非谐泛音簇、长余韵)、暮鼓(低频膜鸣 + 击边噪声) * 与引磬(高音泛音点缀),三种寺院打击声部随机方位错开出现, * 方位随机、音量压在人声之下;2.5 秒淡入。需在用户手势内调用。 */ function startBgm() { if (!bgmCtx) bgmCtx = new (window.AudioContext || window.webkitAudioContext)(); if (bgmCtx.state === "suspended") bgmCtx.resume(); const now = bgmCtx.currentTime; const g = bgmCtx.createGain(); g.gain.setValueAtTime(0, now); // 低通只做轻微软化,保留高频空气感(太低会发闷) const lp = bgmCtx.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 3200; lp.Q.value = 0.4; g.connect(lp); lp.connect(bgmCtx.destination); const base = bgmRoot; // 初始根音,之后由 bgmGlide 慢移 const partials = [[1, 0.45], [2.02, 0.2], [3.01, 0.13], [4.16, 0.08], [5.43, 0.05], [7.2, 0.025]]; const nodes = []; bgmOscs = []; partials.forEach(([ratio, amp], pi) => { const og = bgmCtx.createGain(); og.gain.value = amp; // 泛音左右宽展开 + 极慢声像漂移,空间缓慢呼吸 if (bgmCtx.createStereoPanner) { const pan = bgmCtx.createStereoPanner(); const side = pi % 2 === 0 ? -1 : 1; pan.pan.value = side * 0.6; og.connect(pan); pan.connect(g); const plfo = bgmCtx.createOscillator(); plfo.frequency.value = 0.02 + Math.random() * 0.03; const plg = bgmCtx.createGain(); plg.gain.value = 0.3; plfo.connect(plg); plg.connect(pan.pan); plfo.start(now + pi * 0.5); nodes.push(plfo); } else { og.connect(g); } // 双振荡器 ±3 音分失谐,产生颂钵的拍频颤动 for (const det of [-3, 3]) { const osc = bgmCtx.createOscillator(); osc.type = "sine"; osc.frequency.value = base * ratio; osc.detune.value = det + (Math.random() - 0.5) * 2; osc.connect(og); osc.start(now + Math.random() * 0.3); nodes.push(osc); bgmOscs.push({ osc, ratio }); } // 每个泛音带独立慢速呼吸,避免死板 const lfo = bgmCtx.createOscillator(); lfo.frequency.value = 0.04 + Math.random() * 0.07; const lg = bgmCtx.createGain(); lg.gain.value = amp * 0.4; lfo.connect(lg); lg.connect(og.gain); lfo.start(now); nodes.push(lfo); }); g.gain.linearRampToValueAtTime(0.07, now + 2.5); // 淡入,drone 压低垫底 bgmGain = g; bgmNodes = nodes; // 晨钟:淡入后 4 秒先撞一声,之后每 30~55 秒一声 const scheduleBell = () => { bgmBellTimer = setTimeout(() => { if (!bgmGain) return; bgmBell(); scheduleBell(); }, 30000 + Math.random() * 25000); }; bgmBellTimer = setTimeout(() => { if (!bgmGain) return; bgmBell(); scheduleBell(); }, 4000); // 暮鼓:每 45~80 秒一记,偶尔连击两记(间隔约 2.4s) const scheduleDrum = () => { bgmDrumTimer = setTimeout(() => { if (!bgmGain) return; bgmDrum(); if (Math.random() < 0.45) setTimeout(() => { if (bgmGain) bgmDrum(); }, 2400); scheduleDrum(); }, 45000 + Math.random() * 35000); }; scheduleDrum(); // 引磬:最稀疏,每 60~100 秒一声清越点缀 const scheduleChime = () => { bgmChimeTimer = setTimeout(() => { if (!bgmGain) return; bgmChime(); scheduleChime(); }, 60000 + Math.random() * 40000); }; scheduleChime(); // 首次基频滑变:约 50 秒后 bgmGlideTimer = setTimeout(() => { if (bgmGain) bgmGlide(); }, 50000); } /** 停止背景音乐:1.5 秒淡出后停掉振荡器、钟鼓调度器并断开节点。 */ function stopBgm() { if (bgmBellTimer) { clearTimeout(bgmBellTimer); bgmBellTimer = null; } if (bgmDrumTimer) { clearTimeout(bgmDrumTimer); bgmDrumTimer = null; } if (bgmChimeTimer) { clearTimeout(bgmChimeTimer); bgmChimeTimer = null; } if (bgmGlideTimer) { clearTimeout(bgmGlideTimer); bgmGlideTimer = null; } if (!bgmCtx || !bgmGain) return; const g = bgmGain; const nodes = bgmNodes; const now = bgmCtx.currentTime; g.gain.cancelScheduledValues(now); g.gain.setValueAtTime(g.gain.value, now); g.gain.linearRampToValueAtTime(0, now + 1.5); bgmGain = null; bgmNodes = []; setTimeout(() => { nodes.forEach((n) => { try { n.stop(); } catch { /* 已停止 */ } }); g.disconnect(); }, 1700); } let sutra = null; // 当前经 let segments = []; // 当前经分段 let currentPage = 0; let pageCount = 1; let segEls = []; let dots = []; const currentVoice = () => voiceSelect.value; const pageOf = (i) => Math.floor(i / sutra.pageSize); // ===== 音色下拉 ===== VOICE_GROUPS.forEach((group) => { const target = VOICE_GROUPS.length > 1 ? voiceSelect.appendChild(Object.assign(document.createElement("optgroup"), { label: group.label })) : voiceSelect; group.voices.forEach(([value, label]) => { const opt = document.createElement("option"); opt.value = value; opt.textContent = label; target.appendChild(opt); }); }); voiceSelect.value = "Seren"; // ===== 状态 / 高亮 ===== function setStatus(text, isError = false) { statusEl.textContent = text; statusEl.classList.toggle("error", isError); } function setPlayingUI(on) { playing = on; btnPlay.classList.toggle("playing", on); btnPlay.setAttribute("aria-label", on ? "停止" : "播放"); btnPlay.querySelector(".play-label").textContent = on ? "停 止" : "播 放"; } function highlight(index) { segEls.forEach((el) => { const i = Number(el.dataset.index); el.classList.toggle("playing", i === index); el.classList.toggle("done", i < index); }); dots.forEach((d, i) => { d.classList.toggle("now", i === index); d.classList.toggle("done", index >= 0 && i < index); }); // 跟读滚动:把正在朗读的段落平滑移到显示区中央 const active = segEls.find((el) => Number(el.dataset.index) === index); active?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }); } function clearHighlight() { segEls.forEach((el) => el.classList.remove("playing", "done")); dots.forEach((d) => d.classList.remove("now", "done")); } // ===== 渲染 ===== function renderDots() { progressEl.innerHTML = ""; dots = segments.map(() => { const d = document.createElement("span"); d.className = "dot"; progressEl.appendChild(d); return d; }); } /** 渲染当前页的经文分段(含品名标题)。 */ function renderPage(page) { currentPage = page; sutraEl.innerHTML = ""; const start = page * sutra.pageSize; const end = Math.min(start + sutra.pageSize, segments.length); segEls = []; for (let i = start; i < end; i += 1) { const seg = segments[i]; if (seg.h) { const h = document.createElement("h4"); h.className = "chap"; h.innerHTML = ""; const name = document.createElement("span"); name.className = "chap-name"; name.textContent = seg.h; h.appendChild(name); if (seg.g) { const gloss = document.createElement("span"); gloss.className = "chap-gloss"; gloss.textContent = seg.g; h.appendChild(gloss); } sutraEl.appendChild(h); } const p = document.createElement("p"); p.className = "seg"; p.dataset.index = i; p.textContent = seg.t; p.setAttribute("role", "button"); p.setAttribute("tabindex", "0"); const label = seg.h ? seg.h : `第 ${i + 1} 段`; p.setAttribute("aria-label", `朗读:${label}`); p.addEventListener("click", () => playFrom(i)); p.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); playFrom(i); } }); sutraEl.appendChild(p); segEls.push(p); } sutraEl.scrollTop = 0; sutraEl.scrollLeft = 0; updatePager(); } function updatePager() { const multi = pageCount > 1; pagerEl.hidden = !multi; pageInfo.textContent = `第 ${currentPage + 1} / ${pageCount} 页`; btnPrev.disabled = currentPage === 0; btnNext.disabled = currentPage === pageCount - 1; } /** 手动翻页(停止当前朗读)。 */ function gotoPage(page) { if (page < 0 || page >= pageCount || page === currentPage) return; stopPlayback(); renderPage(page); setStatus(`已翻到第 ${page + 1} / ${pageCount} 页`); } /** 切换经文。 */ function selectSutra(id) { const next = SUTRAS.find((s) => s.id === id); if (!next || next === sutra) return; stopPlayback(); sutra = next; segments = sutra.segments; pageCount = Math.ceil(segments.length / sutra.pageSize); document.title = sutra.name; document.getElementById("sutra-title").textContent = sutra.title; document.getElementById("sutra-sub").textContent = sutra.translator; const sealEl = document.getElementById("seal"); sealEl.textContent = sutra.seal; sealEl.dataset.chars = sutra.seal.length; const src = document.getElementById("sutra-source"); src.textContent = sutra.source; src.href = sutra.sourceUrl; document.querySelectorAll(".sutra-btn").forEach((b) => { b.classList.toggle("active", b.dataset.sutra === id); }); renderDots(); renderPage(0); setStatus(`已切换到《${sutra.name}》· 共 ${segments.length} 段${pageCount > 1 ? ` / ${pageCount} 页` : ""}`); } // ===== 播放控制 ===== /** 取消进行中的请求与播放,令牌递增使旧循环失效。 */ function stopPlayback() { playToken += 1; setPlayingUI(false); if (inflight) { inflight.abort(); inflight = null; } audioEl.pause(); audioEl.ontimeupdate = null; audioEl.onended = null; stopBgm(); clearHighlight(); currentSeg = -1; } /** * 获取指定段的音频 Blob,命中缓存则直接返回。 * @param {number} index 段序号 * @returns {Promise} 音频数据 */ async function fetchSegment(index) { const key = `${sutra.id}|${currentVoice()}|${index}`; if (audioCache.has(key)) return audioCache.get(key); inflight = new AbortController(); try { const resp = await fetch("/api/tts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: segments[index].t, voice: currentVoice() }), signal: inflight.signal, }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.error || `HTTP ${resp.status}`); } const blob = await resp.blob(); audioCache.set(key, blob); return blob; } finally { inflight = null; } } /** * 解析 WAV 文件头,返回 {rate, channels, pcm};格式不符返回 null。 * @param {ArrayBuffer} buf WAV 数据 */ function parseWav(buf) { const v = new DataView(buf); const tag = (o) => v.getUint32(o, true); // 块 ID 按小端读 if (buf.byteLength < 44 || tag(0) !== 0x46464952) return null; // 'RIFF' let off = 12; let fmt = null; let pcm = null; while (off + 8 <= buf.byteLength) { const id = tag(off); const sz = v.getUint32(off + 4, true); if (id === 0x20746d66) { // 'fmt ' fmt = { channels: v.getUint16(off + 10, true), rate: v.getUint32(off + 12, true), bits: v.getUint16(off + 22, true) }; } else if (id === 0x61746164) { // 'data' // 流式编码的 WAV 可能写占位符长度(如 0x7fffffxx),按实际文件长度截断 const realLen = Math.min(sz, buf.byteLength - (off + 8)); pcm = new Uint8Array(buf, off + 8, realLen); break; } off += 8 + sz + (sz % 2); } if (!fmt || !pcm || fmt.bits !== 16) return null; return { rate: fmt.rate, channels: fmt.channels, pcm }; } /** * 将全部段落 PCM 合并为一个 WAV:段间 0.8s 静音、末尾 3s 静音(循环间隔)。 * 合并成单文件后整个诵读只播一个音频,无 JS 事件链,手机锁屏/后台也不中断。 * @param {Blob[]} blobs 各段音频 * @returns {Promise<{url: string, times: number[]}>} 音频地址与各段起始秒 */ async function buildCombined(blobs) { const parts = []; const times = []; let rate = 24000; let channels = 1; let cursor = 0; // 已写入字节数 for (const b of blobs) { const w = parseWav(await b.arrayBuffer()); if (!w) throw new Error("音频格式不支持"); rate = w.rate; channels = w.channels; if (parts.length) { const gap = new Uint8Array(Math.floor(rate * 0.8) * channels * 2); parts.push(gap); cursor += gap.length; } times.push(cursor / (rate * channels * 2)); parts.push(w.pcm); cursor += w.pcm.length; } parts.push(new Uint8Array(Math.floor(rate * 3) * channels * 2)); // 尾部 3s 静音 const total = parts.reduce((s, p) => s + p.length, 0); const out = new ArrayBuffer(44 + total); const v = new DataView(out); const wstr = (o, s) => { for (let k = 0; k < s.length; k += 1) v.setUint8(o + k, s.charCodeAt(k)); }; wstr(0, "RIFF"); v.setUint32(4, 36 + total, true); wstr(8, "WAVE"); wstr(12, "fmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, channels, true); v.setUint32(24, rate, true); v.setUint32(28, rate * channels * 2, true); v.setUint16(32, channels * 2, true); v.setUint16(34, 16, true); wstr(36, "data"); v.setUint32(40, total, true); let o = 44; for (const p of parts) { new Uint8Array(out, o, p.length).set(p); o += p.length; } return { url: URL.createObjectURL(new Blob([out], { type: "audio/wav" })), times }; } /** 播放进度回调:按当前时间定位段号,高亮并自动翻页滚动。 */ function onTimeUpdate() { const t = audioEl.currentTime; let i = segTimes.length - 1; while (i > 0 && segTimes[i] > t) i -= 1; if (i === currentSeg) return; currentSeg = i; const targetPage = pageOf(i); if (targetPage !== currentPage) renderPage(targetPage); highlight(i); } /** * 从第 index 段开始连续朗读到全文结束;开启循环时 audio.loop 原生回卷。 * 先并发补齐所有段音频再合并为单个 WAV 播放,中途无 JS 事件链衔接, * 手机锁屏/后台不会中断;高亮靠 timeupdate 按 segTimes 时间轴定位。 */ async function playFrom(index) { stopPlayback(); const token = ++playToken; setPlayingUI(true); setStatus("正在准备音频…"); const blobs = new Array(segments.length); let next = 0; let done = 0; const worker = async () => { while (next < segments.length && token === playToken) { const i = next; next += 1; blobs[i] = await fetchSegment(i); done += 1; setStatus(`正在准备音频 ${done} / ${segments.length}…`); } }; try { await Promise.all([worker(), worker(), worker(), worker()]); if (token !== playToken) return; const built = await buildCombined(blobs); if (token !== playToken) return; segTimes = built.times; currentSeg = -1; audioEl.src = built.url; audioEl.loop = loopMode; audioEl.ontimeupdate = onTimeUpdate; audioEl.onended = () => { setPlayingUI(false); setStatus("诵读完毕"); stopBgm(); clearHighlight(); }; audioEl.currentTime = built.times[index]; await audioEl.play(); if (bgmOn) startBgm(); // play() 成功后起步背景音乐(用户手势上下文内) setStatus(`正在朗读 · ${currentVoice()}`); } catch (err) { if (err.name === "AbortError") return; // 已被停止/新播放取代 setStatus(`语音合成失败:${err.message}`, true); setPlayingUI(false); clearHighlight(); } } /** 循环诵读开关:开时读完全文自动回到第一段继续。 */ btnLoop.addEventListener("click", () => { loopMode = !loopMode; audioEl.loop = loopMode; // 播放中切换立即生效 btnLoop.classList.toggle("active", loopMode); btnLoop.setAttribute("aria-pressed", String(loopMode)); setStatus(loopMode ? "循环诵读:开" : "循环诵读:关"); }); /** 背景音乐开关:播放中开/关即时淡入淡出;未播放时只记录状态,下次播放生效。 */ btnBgm.addEventListener("click", () => { bgmOn = !bgmOn; btnBgm.classList.toggle("active", bgmOn); btnBgm.setAttribute("aria-pressed", String(bgmOn)); if (bgmOn && playing) startBgm(); if (!bgmOn) stopBgm(); setStatus(bgmOn ? "背景音乐:开" : "背景音乐:关"); }); btnPlay.addEventListener("click", () => { if (playing || inflight) { stopPlayback(); setStatus("已停止"); } else { playFrom(0); } }); voiceSelect.addEventListener("change", () => { if (playing || inflight) stopPlayback(); setStatus(`已切换音色:${voiceSelect.value},点击播放开始诵读`); }); // 移动端设置区折叠/展开(齿轮图标按钮,桌面端不可见) btnExpand.addEventListener("click", () => { const expanded = playerCard.classList.toggle("expanded"); btnExpand.classList.toggle("open", expanded); btnExpand.querySelector(".expand-label").textContent = expanded ? "收起" : "设置"; btnExpand.setAttribute("aria-expanded", String(expanded)); }); btnPrev.addEventListener("click", () => gotoPage(currentPage - 1)); btnNext.addEventListener("click", () => gotoPage(currentPage + 1)); document.querySelectorAll(".sutra-btn").forEach((btn) => { btn.addEventListener("click", () => selectSutra(btn.dataset.sutra)); }); // 键盘 ← → 翻页(下拉框聚焦时不拦截) document.addEventListener("keydown", (e) => { if (e.target.tagName === "SELECT") return; if (e.key === "ArrowLeft") gotoPage(currentPage - 1); if (e.key === "ArrowRight") gotoPage(currentPage + 1); }); // ===== 字号与排版 ===== document.querySelectorAll(".size-btn").forEach((btn) => { btn.addEventListener("click", () => { document.querySelectorAll(".size-btn").forEach((b) => b.classList.remove("active")); btn.classList.add("active"); sutraEl.classList.remove("size-sm", "size-md", "size-lg"); sutraEl.classList.add(`size-${btn.dataset.size}`); }); }); const mobileMQ = matchMedia("(max-width: 640px)"); btnLayout.addEventListener("click", () => { const vertical = sutraEl.classList.toggle("vertical"); btnLayout.textContent = vertical ? "切换横排" : "切换竖排"; btnLayout.classList.toggle("active", vertical); }); // 移动端不提供竖排:窗口收窄时若处于竖排则切回横排 mobileMQ.addEventListener("change", () => { if (mobileMQ.matches && sutraEl.classList.contains("vertical")) { btnLayout.click(); } }); // ===== 初始化(支持 #jingang / #vertical 锚点;手机端忽略竖排) ===== if (location.hash.includes("vertical") && !mobileMQ.matches) { btnLayout.click(); } selectSutra(location.hash.includes("jingang") ? "jingang" : "xinjing");