Commit | Line | Data |
---|---|---|
d60e4f1e JB |
1 | // SPDX-License-Identifier: (GPL-2.0 OR MIT) |
2 | // | |
3 | // Copyright (c) 2018 BayLibre, SAS. | |
4 | // Author: Jerome Brunet <jbrunet@baylibre.com> | |
5 | ||
6 | #include <linux/clk.h> | |
7 | #include <linux/module.h> | |
8 | #include <linux/of_platform.h> | |
9 | #include <sound/pcm_params.h> | |
10 | #include <sound/soc.h> | |
11 | #include <sound/soc-dai.h> | |
12 | ||
13 | #include "axg-tdm.h" | |
14 | ||
15 | enum { | |
16 | TDM_IFACE_PAD, | |
17 | TDM_IFACE_LOOPBACK, | |
18 | }; | |
19 | ||
20 | static unsigned int axg_tdm_slots_total(u32 *mask) | |
21 | { | |
22 | unsigned int slots = 0; | |
23 | int i; | |
24 | ||
25 | if (!mask) | |
26 | return 0; | |
27 | ||
28 | /* Count the total number of slots provided by all 4 lanes */ | |
29 | for (i = 0; i < AXG_TDM_NUM_LANES; i++) | |
30 | slots += hweight32(mask[i]); | |
31 | ||
32 | return slots; | |
33 | } | |
34 | ||
35 | int axg_tdm_set_tdm_slots(struct snd_soc_dai *dai, u32 *tx_mask, | |
36 | u32 *rx_mask, unsigned int slots, | |
37 | unsigned int slot_width) | |
38 | { | |
39 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
40 | struct axg_tdm_stream *tx = (struct axg_tdm_stream *) | |
41 | dai->playback_dma_data; | |
42 | struct axg_tdm_stream *rx = (struct axg_tdm_stream *) | |
43 | dai->capture_dma_data; | |
44 | unsigned int tx_slots, rx_slots; | |
302df269 | 45 | unsigned int fmt = 0; |
d60e4f1e JB |
46 | |
47 | tx_slots = axg_tdm_slots_total(tx_mask); | |
48 | rx_slots = axg_tdm_slots_total(rx_mask); | |
49 | ||
50 | /* We should at least have a slot for a valid interface */ | |
51 | if (!tx_slots && !rx_slots) { | |
52 | dev_err(dai->dev, "interface has no slot\n"); | |
53 | return -EINVAL; | |
54 | } | |
55 | ||
d60e4f1e JB |
56 | iface->slots = slots; |
57 | ||
58 | switch (slot_width) { | |
59 | case 0: | |
302df269 | 60 | slot_width = 32; |
df561f66 | 61 | fallthrough; |
d60e4f1e | 62 | case 32: |
302df269 | 63 | fmt |= SNDRV_PCM_FMTBIT_S32_LE; |
df561f66 | 64 | fallthrough; |
302df269 JB |
65 | case 24: |
66 | fmt |= SNDRV_PCM_FMTBIT_S24_LE; | |
67 | fmt |= SNDRV_PCM_FMTBIT_S20_LE; | |
df561f66 | 68 | fallthrough; |
302df269 JB |
69 | case 16: |
70 | fmt |= SNDRV_PCM_FMTBIT_S16_LE; | |
df561f66 | 71 | fallthrough; |
302df269 JB |
72 | case 8: |
73 | fmt |= SNDRV_PCM_FMTBIT_S8; | |
d60e4f1e JB |
74 | break; |
75 | default: | |
76 | dev_err(dai->dev, "unsupported slot width: %d\n", slot_width); | |
77 | return -EINVAL; | |
78 | } | |
79 | ||
302df269 JB |
80 | iface->slot_width = slot_width; |
81 | ||
82 | /* Amend the dai driver and let dpcm merge do its job */ | |
83 | if (tx) { | |
84 | tx->mask = tx_mask; | |
85 | dai->driver->playback.channels_max = tx_slots; | |
86 | dai->driver->playback.formats = fmt; | |
87 | } | |
88 | ||
89 | if (rx) { | |
90 | rx->mask = rx_mask; | |
91 | dai->driver->capture.channels_max = rx_slots; | |
92 | dai->driver->capture.formats = fmt; | |
93 | } | |
94 | ||
d60e4f1e JB |
95 | return 0; |
96 | } | |
97 | EXPORT_SYMBOL_GPL(axg_tdm_set_tdm_slots); | |
98 | ||
99 | static int axg_tdm_iface_set_sysclk(struct snd_soc_dai *dai, int clk_id, | |
100 | unsigned int freq, int dir) | |
101 | { | |
102 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
103 | int ret = -ENOTSUPP; | |
104 | ||
105 | if (dir == SND_SOC_CLOCK_OUT && clk_id == 0) { | |
106 | if (!iface->mclk) { | |
107 | dev_warn(dai->dev, "master clock not provided\n"); | |
108 | } else { | |
109 | ret = clk_set_rate(iface->mclk, freq); | |
110 | if (!ret) | |
111 | iface->mclk_rate = freq; | |
112 | } | |
113 | } | |
114 | ||
115 | return ret; | |
116 | } | |
117 | ||
118 | static int axg_tdm_iface_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) | |
119 | { | |
120 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
121 | ||
6878ba91 JB |
122 | switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { |
123 | case SND_SOC_DAIFMT_CBS_CFS: | |
124 | if (!iface->mclk) { | |
125 | dev_err(dai->dev, "cpu clock master: mclk missing\n"); | |
126 | return -ENODEV; | |
127 | } | |
128 | break; | |
129 | ||
130 | case SND_SOC_DAIFMT_CBM_CFM: | |
131 | break; | |
132 | ||
133 | case SND_SOC_DAIFMT_CBS_CFM: | |
134 | case SND_SOC_DAIFMT_CBM_CFS: | |
d60e4f1e | 135 | dev_err(dai->dev, "only CBS_CFS and CBM_CFM are supported\n"); |
df561f66 | 136 | fallthrough; |
6878ba91 | 137 | default: |
d60e4f1e JB |
138 | return -EINVAL; |
139 | } | |
140 | ||
d60e4f1e JB |
141 | iface->fmt = fmt; |
142 | return 0; | |
143 | } | |
144 | ||
145 | static int axg_tdm_iface_startup(struct snd_pcm_substream *substream, | |
146 | struct snd_soc_dai *dai) | |
147 | { | |
148 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
149 | struct axg_tdm_stream *ts = | |
150 | snd_soc_dai_get_dma_data(dai, substream); | |
151 | int ret; | |
152 | ||
153 | if (!axg_tdm_slots_total(ts->mask)) { | |
154 | dev_err(dai->dev, "interface has not slots\n"); | |
155 | return -EINVAL; | |
156 | } | |
157 | ||
158 | /* Apply component wide rate symmetry */ | |
1f79aab5 | 159 | if (snd_soc_component_active(dai->component)) { |
d60e4f1e JB |
160 | ret = snd_pcm_hw_constraint_single(substream->runtime, |
161 | SNDRV_PCM_HW_PARAM_RATE, | |
162 | iface->rate); | |
163 | if (ret < 0) { | |
164 | dev_err(dai->dev, | |
165 | "can't set iface rate constraint\n"); | |
166 | return ret; | |
167 | } | |
168 | } | |
169 | ||
170 | return 0; | |
171 | } | |
172 | ||
173 | static int axg_tdm_iface_set_stream(struct snd_pcm_substream *substream, | |
174 | struct snd_pcm_hw_params *params, | |
175 | struct snd_soc_dai *dai) | |
176 | { | |
177 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
178 | struct axg_tdm_stream *ts = snd_soc_dai_get_dma_data(dai, substream); | |
179 | unsigned int channels = params_channels(params); | |
180 | unsigned int width = params_width(params); | |
181 | ||
182 | /* Save rate and sample_bits for component symmetry */ | |
183 | iface->rate = params_rate(params); | |
184 | ||
185 | /* Make sure this interface can cope with the stream */ | |
186 | if (axg_tdm_slots_total(ts->mask) < channels) { | |
187 | dev_err(dai->dev, "not enough slots for channels\n"); | |
188 | return -EINVAL; | |
189 | } | |
190 | ||
191 | if (iface->slot_width < width) { | |
192 | dev_err(dai->dev, "incompatible slots width for stream\n"); | |
193 | return -EINVAL; | |
194 | } | |
195 | ||
196 | /* Save the parameter for tdmout/tdmin widgets */ | |
197 | ts->physical_width = params_physical_width(params); | |
198 | ts->width = params_width(params); | |
199 | ts->channels = params_channels(params); | |
200 | ||
201 | return 0; | |
202 | } | |
203 | ||
204 | static int axg_tdm_iface_set_lrclk(struct snd_soc_dai *dai, | |
205 | struct snd_pcm_hw_params *params) | |
206 | { | |
207 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
208 | unsigned int ratio_num; | |
209 | int ret; | |
210 | ||
211 | ret = clk_set_rate(iface->lrclk, params_rate(params)); | |
212 | if (ret) { | |
213 | dev_err(dai->dev, "setting sample clock failed: %d\n", ret); | |
214 | return ret; | |
215 | } | |
216 | ||
217 | switch (iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK) { | |
218 | case SND_SOC_DAIFMT_I2S: | |
219 | case SND_SOC_DAIFMT_LEFT_J: | |
220 | case SND_SOC_DAIFMT_RIGHT_J: | |
221 | /* 50% duty cycle ratio */ | |
222 | ratio_num = 1; | |
223 | break; | |
224 | ||
225 | case SND_SOC_DAIFMT_DSP_A: | |
226 | case SND_SOC_DAIFMT_DSP_B: | |
227 | /* | |
228 | * A zero duty cycle ratio will result in setting the mininum | |
229 | * ratio possible which, for this clock, is 1 cycle of the | |
230 | * parent bclk clock high and the rest low, This is exactly | |
231 | * what we want here. | |
232 | */ | |
233 | ratio_num = 0; | |
234 | break; | |
235 | ||
236 | default: | |
237 | return -EINVAL; | |
238 | } | |
239 | ||
240 | ret = clk_set_duty_cycle(iface->lrclk, ratio_num, 2); | |
241 | if (ret) { | |
242 | dev_err(dai->dev, | |
243 | "setting sample clock duty cycle failed: %d\n", ret); | |
244 | return ret; | |
245 | } | |
246 | ||
247 | /* Set sample clock inversion */ | |
248 | ret = clk_set_phase(iface->lrclk, | |
249 | axg_tdm_lrclk_invert(iface->fmt) ? 180 : 0); | |
250 | if (ret) { | |
251 | dev_err(dai->dev, | |
252 | "setting sample clock phase failed: %d\n", ret); | |
253 | return ret; | |
254 | } | |
255 | ||
256 | return 0; | |
257 | } | |
258 | ||
259 | static int axg_tdm_iface_set_sclk(struct snd_soc_dai *dai, | |
260 | struct snd_pcm_hw_params *params) | |
261 | { | |
262 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
263 | unsigned long srate; | |
264 | int ret; | |
265 | ||
266 | srate = iface->slots * iface->slot_width * params_rate(params); | |
267 | ||
268 | if (!iface->mclk_rate) { | |
269 | /* If no specific mclk is requested, default to bit clock * 4 */ | |
270 | clk_set_rate(iface->mclk, 4 * srate); | |
271 | } else { | |
272 | /* Check if we can actually get the bit clock from mclk */ | |
273 | if (iface->mclk_rate % srate) { | |
274 | dev_err(dai->dev, | |
275 | "can't derive sclk %lu from mclk %lu\n", | |
276 | srate, iface->mclk_rate); | |
277 | return -EINVAL; | |
278 | } | |
279 | } | |
280 | ||
281 | ret = clk_set_rate(iface->sclk, srate); | |
282 | if (ret) { | |
283 | dev_err(dai->dev, "setting bit clock failed: %d\n", ret); | |
284 | return ret; | |
285 | } | |
286 | ||
287 | /* Set the bit clock inversion */ | |
288 | ret = clk_set_phase(iface->sclk, | |
289 | axg_tdm_sclk_invert(iface->fmt) ? 0 : 180); | |
290 | if (ret) { | |
291 | dev_err(dai->dev, "setting bit clock phase failed: %d\n", ret); | |
292 | return ret; | |
293 | } | |
294 | ||
295 | return ret; | |
296 | } | |
297 | ||
298 | static int axg_tdm_iface_hw_params(struct snd_pcm_substream *substream, | |
299 | struct snd_pcm_hw_params *params, | |
300 | struct snd_soc_dai *dai) | |
301 | { | |
302 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
303 | int ret; | |
304 | ||
305 | switch (iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK) { | |
306 | case SND_SOC_DAIFMT_I2S: | |
307 | case SND_SOC_DAIFMT_LEFT_J: | |
308 | case SND_SOC_DAIFMT_RIGHT_J: | |
309 | if (iface->slots > 2) { | |
310 | dev_err(dai->dev, "bad slot number for format: %d\n", | |
311 | iface->slots); | |
312 | return -EINVAL; | |
313 | } | |
314 | break; | |
315 | ||
05113483 JB |
316 | case SND_SOC_DAIFMT_DSP_A: |
317 | case SND_SOC_DAIFMT_DSP_B: | |
d60e4f1e JB |
318 | break; |
319 | ||
320 | default: | |
321 | dev_err(dai->dev, "unsupported dai format\n"); | |
322 | return -EINVAL; | |
323 | } | |
324 | ||
325 | ret = axg_tdm_iface_set_stream(substream, params, dai); | |
326 | if (ret) | |
327 | return ret; | |
328 | ||
6878ba91 JB |
329 | if ((iface->fmt & SND_SOC_DAIFMT_MASTER_MASK) == |
330 | SND_SOC_DAIFMT_CBS_CFS) { | |
d60e4f1e JB |
331 | ret = axg_tdm_iface_set_sclk(dai, params); |
332 | if (ret) | |
333 | return ret; | |
334 | ||
335 | ret = axg_tdm_iface_set_lrclk(dai, params); | |
336 | if (ret) | |
337 | return ret; | |
338 | } | |
339 | ||
340 | return 0; | |
341 | } | |
342 | ||
343 | static int axg_tdm_iface_hw_free(struct snd_pcm_substream *substream, | |
344 | struct snd_soc_dai *dai) | |
345 | { | |
346 | struct axg_tdm_stream *ts = snd_soc_dai_get_dma_data(dai, substream); | |
347 | ||
348 | /* Stop all attached formatters */ | |
349 | axg_tdm_stream_stop(ts); | |
350 | ||
351 | return 0; | |
352 | } | |
353 | ||
354 | static int axg_tdm_iface_prepare(struct snd_pcm_substream *substream, | |
355 | struct snd_soc_dai *dai) | |
356 | { | |
357 | struct axg_tdm_stream *ts = snd_soc_dai_get_dma_data(dai, substream); | |
358 | ||
359 | /* Force all attached formatters to update */ | |
360 | return axg_tdm_stream_reset(ts); | |
361 | } | |
362 | ||
363 | static int axg_tdm_iface_remove_dai(struct snd_soc_dai *dai) | |
364 | { | |
365 | if (dai->capture_dma_data) | |
366 | axg_tdm_stream_free(dai->capture_dma_data); | |
367 | ||
368 | if (dai->playback_dma_data) | |
369 | axg_tdm_stream_free(dai->playback_dma_data); | |
370 | ||
371 | return 0; | |
372 | } | |
373 | ||
374 | static int axg_tdm_iface_probe_dai(struct snd_soc_dai *dai) | |
375 | { | |
376 | struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai); | |
377 | ||
378 | if (dai->capture_widget) { | |
379 | dai->capture_dma_data = axg_tdm_stream_alloc(iface); | |
380 | if (!dai->capture_dma_data) | |
381 | return -ENOMEM; | |
382 | } | |
383 | ||
384 | if (dai->playback_widget) { | |
385 | dai->playback_dma_data = axg_tdm_stream_alloc(iface); | |
386 | if (!dai->playback_dma_data) { | |
387 | axg_tdm_iface_remove_dai(dai); | |
388 | return -ENOMEM; | |
389 | } | |
390 | } | |
391 | ||
392 | return 0; | |
393 | } | |
394 | ||
395 | static const struct snd_soc_dai_ops axg_tdm_iface_ops = { | |
396 | .set_sysclk = axg_tdm_iface_set_sysclk, | |
397 | .set_fmt = axg_tdm_iface_set_fmt, | |
398 | .startup = axg_tdm_iface_startup, | |
399 | .hw_params = axg_tdm_iface_hw_params, | |
400 | .prepare = axg_tdm_iface_prepare, | |
401 | .hw_free = axg_tdm_iface_hw_free, | |
402 | }; | |
403 | ||
404 | /* TDM Backend DAIs */ | |
405 | static const struct snd_soc_dai_driver axg_tdm_iface_dai_drv[] = { | |
406 | [TDM_IFACE_PAD] = { | |
407 | .name = "TDM Pad", | |
408 | .playback = { | |
409 | .stream_name = "Playback", | |
410 | .channels_min = 1, | |
411 | .channels_max = AXG_TDM_CHANNEL_MAX, | |
412 | .rates = AXG_TDM_RATES, | |
413 | .formats = AXG_TDM_FORMATS, | |
414 | }, | |
415 | .capture = { | |
416 | .stream_name = "Capture", | |
417 | .channels_min = 1, | |
418 | .channels_max = AXG_TDM_CHANNEL_MAX, | |
419 | .rates = AXG_TDM_RATES, | |
420 | .formats = AXG_TDM_FORMATS, | |
421 | }, | |
422 | .id = TDM_IFACE_PAD, | |
423 | .ops = &axg_tdm_iface_ops, | |
424 | .probe = axg_tdm_iface_probe_dai, | |
425 | .remove = axg_tdm_iface_remove_dai, | |
426 | }, | |
427 | [TDM_IFACE_LOOPBACK] = { | |
428 | .name = "TDM Loopback", | |
429 | .capture = { | |
430 | .stream_name = "Loopback", | |
431 | .channels_min = 1, | |
432 | .channels_max = AXG_TDM_CHANNEL_MAX, | |
433 | .rates = AXG_TDM_RATES, | |
434 | .formats = AXG_TDM_FORMATS, | |
435 | }, | |
436 | .id = TDM_IFACE_LOOPBACK, | |
437 | .ops = &axg_tdm_iface_ops, | |
438 | .probe = axg_tdm_iface_probe_dai, | |
439 | .remove = axg_tdm_iface_remove_dai, | |
440 | }, | |
441 | }; | |
442 | ||
443 | static int axg_tdm_iface_set_bias_level(struct snd_soc_component *component, | |
444 | enum snd_soc_bias_level level) | |
445 | { | |
446 | struct axg_tdm_iface *iface = snd_soc_component_get_drvdata(component); | |
447 | enum snd_soc_bias_level now = | |
448 | snd_soc_component_get_bias_level(component); | |
449 | int ret = 0; | |
450 | ||
451 | switch (level) { | |
452 | case SND_SOC_BIAS_PREPARE: | |
453 | if (now == SND_SOC_BIAS_STANDBY) | |
454 | ret = clk_prepare_enable(iface->mclk); | |
455 | break; | |
456 | ||
457 | case SND_SOC_BIAS_STANDBY: | |
458 | if (now == SND_SOC_BIAS_PREPARE) | |
459 | clk_disable_unprepare(iface->mclk); | |
460 | break; | |
461 | ||
462 | case SND_SOC_BIAS_OFF: | |
463 | case SND_SOC_BIAS_ON: | |
464 | break; | |
465 | } | |
466 | ||
467 | return ret; | |
468 | } | |
469 | ||
470 | static const struct snd_soc_component_driver axg_tdm_iface_component_drv = { | |
471 | .set_bias_level = axg_tdm_iface_set_bias_level, | |
472 | }; | |
473 | ||
474 | static const struct of_device_id axg_tdm_iface_of_match[] = { | |
475 | { .compatible = "amlogic,axg-tdm-iface", }, | |
476 | {} | |
477 | }; | |
478 | MODULE_DEVICE_TABLE(of, axg_tdm_iface_of_match); | |
479 | ||
480 | static int axg_tdm_iface_probe(struct platform_device *pdev) | |
481 | { | |
482 | struct device *dev = &pdev->dev; | |
483 | struct snd_soc_dai_driver *dai_drv; | |
484 | struct axg_tdm_iface *iface; | |
485 | int ret, i; | |
486 | ||
487 | iface = devm_kzalloc(dev, sizeof(*iface), GFP_KERNEL); | |
488 | if (!iface) | |
489 | return -ENOMEM; | |
490 | platform_set_drvdata(pdev, iface); | |
491 | ||
492 | /* | |
493 | * Duplicate dai driver: depending on the slot masks configuration | |
494 | * We'll change the number of channel provided by DAI stream, so dpcm | |
495 | * channel merge can be done properly | |
496 | */ | |
497 | dai_drv = devm_kcalloc(dev, ARRAY_SIZE(axg_tdm_iface_dai_drv), | |
498 | sizeof(*dai_drv), GFP_KERNEL); | |
499 | if (!dai_drv) | |
500 | return -ENOMEM; | |
501 | ||
502 | for (i = 0; i < ARRAY_SIZE(axg_tdm_iface_dai_drv); i++) | |
503 | memcpy(&dai_drv[i], &axg_tdm_iface_dai_drv[i], | |
504 | sizeof(*dai_drv)); | |
505 | ||
506 | /* Bit clock provided on the pad */ | |
507 | iface->sclk = devm_clk_get(dev, "sclk"); | |
508 | if (IS_ERR(iface->sclk)) { | |
509 | ret = PTR_ERR(iface->sclk); | |
510 | if (ret != -EPROBE_DEFER) | |
511 | dev_err(dev, "failed to get sclk: %d\n", ret); | |
512 | return ret; | |
513 | } | |
514 | ||
515 | /* Sample clock provided on the pad */ | |
516 | iface->lrclk = devm_clk_get(dev, "lrclk"); | |
517 | if (IS_ERR(iface->lrclk)) { | |
518 | ret = PTR_ERR(iface->lrclk); | |
519 | if (ret != -EPROBE_DEFER) | |
520 | dev_err(dev, "failed to get lrclk: %d\n", ret); | |
521 | return ret; | |
522 | } | |
523 | ||
524 | /* | |
525 | * mclk maybe be missing when the cpu dai is in slave mode and | |
526 | * the codec does not require it to provide a master clock. | |
527 | * At this point, ignore the error if mclk is missing. We'll | |
528 | * throw an error if the cpu dai is master and mclk is missing | |
529 | */ | |
530 | iface->mclk = devm_clk_get(dev, "mclk"); | |
531 | if (IS_ERR(iface->mclk)) { | |
532 | ret = PTR_ERR(iface->mclk); | |
533 | if (ret == -ENOENT) { | |
534 | iface->mclk = NULL; | |
535 | } else { | |
536 | if (ret != -EPROBE_DEFER) | |
537 | dev_err(dev, "failed to get mclk: %d\n", ret); | |
538 | return ret; | |
539 | } | |
540 | } | |
541 | ||
542 | return devm_snd_soc_register_component(dev, | |
543 | &axg_tdm_iface_component_drv, dai_drv, | |
544 | ARRAY_SIZE(axg_tdm_iface_dai_drv)); | |
545 | } | |
546 | ||
547 | static struct platform_driver axg_tdm_iface_pdrv = { | |
548 | .probe = axg_tdm_iface_probe, | |
549 | .driver = { | |
550 | .name = "axg-tdm-iface", | |
551 | .of_match_table = axg_tdm_iface_of_match, | |
552 | }, | |
553 | }; | |
554 | module_platform_driver(axg_tdm_iface_pdrv); | |
555 | ||
556 | MODULE_DESCRIPTION("Amlogic AXG TDM interface driver"); | |
557 | MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>"); | |
558 | MODULE_LICENSE("GPL v2"); |