A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1/* MikMod sound library
2 (c) 1998, 1999, 2000, 2001, 2002 Miodrag Vallat and others - see file
3 AUTHORS for complete list.
4
5 This library is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Library General Public License as
7 published by the Free Software Foundation; either version 2 of
8 the License, or (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public
16 License along with this library; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
18 02111-1307, USA.
19*/
20
21/*==============================================================================
22
23 DMP Advanced Module Format loader
24
25==============================================================================*/
26
27#ifdef HAVE_CONFIG_H
28#include "config.h"
29#endif
30
31#ifdef HAVE_UNISTD_H
32#include <unistd.h>
33#endif
34
35#include <stdio.h>
36#ifdef HAVE_MEMORY_H
37#include <memory.h>
38#endif
39#include <string.h>
40
41#include "mikmod_internals.h"
42
43#ifdef SUNOS
44extern int fprintf(FILE *, const char *, ...);
45#endif
46
47/*========== Module structure */
48
49typedef struct AMFHEADER {
50 UBYTE id[3]; /* AMF file marker */
51 UBYTE version; /* upper major, lower nibble minor version number */
52 CHAR songname[32]; /* ASCIIZ songname */
53 UBYTE numsamples; /* number of samples saved */
54 UBYTE numorders;
55 UWORD numtracks; /* number of tracks saved */
56 UBYTE numchannels; /* number of channels used */
57 SBYTE panpos[32]; /* voice pan positions */
58 UBYTE songbpm;
59 UBYTE songspd;
60} AMFHEADER;
61
62typedef struct AMFSAMPLE {
63 UBYTE type;
64 CHAR samplename[32];
65 CHAR filename[13];
66 ULONG offset;
67 ULONG length;
68 UWORD c2spd;
69 UBYTE volume;
70 ULONG reppos;
71 ULONG repend;
72} AMFSAMPLE;
73
74typedef struct AMFNOTE {
75 UBYTE note,instr,volume,fxcnt;
76 UBYTE effect[3];
77 SBYTE parameter[3];
78} AMFNOTE;
79
80/*========== Loader variables */
81
82static AMFHEADER *mh = NULL;
83#define AMFTEXTLEN 22
84static CHAR AMF_Version[AMFTEXTLEN+1] = "DSMI Module Format 0.0";
85static AMFNOTE *track = NULL;
86
87/*========== Loader code */
88
89static int AMF_Test(void)
90{
91 UBYTE id[3],ver;
92
93 if(!_mm_read_UBYTES(id,3,modreader)) return 0;
94 if(memcmp(id,"AMF",3)) return 0;
95
96 ver=_mm_read_UBYTE(modreader);
97 if((ver>=8)&&(ver<=14)) return 1;
98 return 0;
99}
100
101static int AMF_Init(void)
102{
103 if(!(mh=(AMFHEADER*)MikMod_malloc(sizeof(AMFHEADER)))) return 0;
104 if(!(track=(AMFNOTE*)MikMod_calloc(64,sizeof(AMFNOTE)))) return 0;
105
106 return 1;
107}
108
109static void AMF_Cleanup(void)
110{
111 MikMod_free(mh);
112 MikMod_free(track);
113 mh=NULL;
114 track=NULL;
115}
116
117/* Some older version 1.0 AMFs contain an anomaly where the sample length is
118 * a DWORD, the loop start is a WORD, and the loop end is missing. Later AMF
119 * 1.0 modules and up contain all three as DWORDs, and earlier versions have
120 * all three as WORDs. This function tries to detect this edge case in the
121 * instruments table. This should only be called on 1.0 AMFs.
122 */
123static int AMF_ScanV10Instruments(MREADER *r, unsigned int numins)
124{
125 SLONG resetpos;
126 int res = 0;
127 char str[32];
128 ULONG idx, len, start, end;
129 UBYTE type, vol;
130 UWORD c2spd;
131 unsigned int i;
132
133 resetpos = _mm_ftell(r);
134 if(resetpos < 0) return 0;
135
136 for(i = 0; i < numins; i++) {
137 type = _mm_read_UBYTE(r); /* type: should be 0 or 1 */
138 _mm_read_string(str, 32, r); /* name */
139 _mm_read_string(str, 13, r); /* filename */
140 idx = _mm_read_I_ULONG(r); /* index (should be <= numins) */
141 len = _mm_read_I_ULONG(r);
142 c2spd = _mm_read_I_UWORD(r); /* should be > 0 */
143 vol = _mm_read_UBYTE(r); /* should be [0,0x40] */
144 start = _mm_read_I_ULONG(r); /* should be <= len */
145 end = _mm_read_I_ULONG(r); /* should be <= len */
146
147 if((type != 0 && type != 1) || (idx > numins) || (c2spd == 0) ||
148 (vol > 0x40) || (start > len) || (end > len)) {
149 res = 1;
150 break;
151 }
152
153 }
154 _mm_fseek(r, resetpos, SEEK_SET);
155 return res;
156}
157
158static int AMF_UnpackTrack(MREADER *r)
159{
160 ULONG tracksize;
161 UBYTE row,cmd;
162 SBYTE arg;
163
164 /* empty track */
165 memset(track,0,64*sizeof(AMFNOTE));
166
167 /* read packed track */
168 if (r) {
169 tracksize=_mm_read_I_UWORD(r);
170
171 /* The original code in DSMI library read the byte,
172 but it is not used, so we won't either */
173// tracksize+=((ULONG)_mm_read_UBYTE(r))<<16;
174 (void)_mm_read_UBYTE(r);
175
176 if (tracksize)
177 while(tracksize--) {
178 row=_mm_read_UBYTE(r);
179 cmd=_mm_read_UBYTE(r);
180 arg=_mm_read_SBYTE(r);
181 /* unexpected end of track */
182 if(!tracksize) {
183 if((row==0xff)&&(cmd==0xff)&&(arg==-1))
184 break;
185 /* the last triplet should be FF FF FF, but this is not
186 always the case... maybe a bug in m2amf ?
187 else
188 return 0;
189 */
190
191 }
192 /* invalid row (probably unexpected end of row) */
193 if (row>=64) {
194 _mm_fseek(modreader, tracksize * 3, SEEK_CUR);
195 return 1;
196 }
197 if (cmd<0x7f) {
198 /* note, vol */
199 /* Note that 0xff values mean this note was not originally
200 accomanied by a volume event. The +1 here causes it to
201 overflow to 0, which will then (correctly) be ignored later. */
202 track[row].note=cmd;
203 track[row].volume=(UBYTE)arg+1;
204 } else
205 if (cmd==0x7f) {
206 /* AMF.TXT claims this should duplicate the previous row, but
207 this is a lie. This note value is used to communicate to
208 DSMI that the current playing note should be updated when
209 an instrument is used with no note. This can be ignored. */
210 } else
211 if (cmd==0x80) {
212 /* instr */
213 track[row].instr=arg+1;
214 } else
215 if (cmd==0x83) {
216 /* volume without note */
217 track[row].volume=(UBYTE)arg+1;
218 } else
219 if (cmd==0xff) {
220 /* apparently, some M2AMF version fail to estimate the
221 size of the compressed patterns correctly, and end
222 up with blanks, i.e. dead triplets. Those are marked
223 with cmd == 0xff. Let's ignore them. */
224 } else
225 if(track[row].fxcnt<3) {
226 /* effect, param */
227 if(cmd>0x97) {
228 /* Instead of failing, we just ignore unknown effects.
229 This will load the "escape from dulce base" module */
230 continue;
231 }
232 track[row].effect[track[row].fxcnt]=cmd&0x7f;
233 track[row].parameter[track[row].fxcnt]=arg;
234 track[row].fxcnt++;
235 } else
236 return 0;
237 }
238 }
239 return 1;
240}
241
242static UBYTE *AMF_ConvertTrack(void)
243{
244 int row,fx4memory=0;
245
246 /* convert track */
247 UniReset();
248 for (row=0;row<64;row++) {
249 if (track[row].instr) UniInstrument(track[row].instr-1);
250 if (track[row].note>OCTAVE) UniNote(track[row].note-OCTAVE);
251
252 /* AMF effects */
253 while(track[row].fxcnt--) {
254 SBYTE inf=track[row].parameter[track[row].fxcnt];
255
256 switch(track[row].effect[track[row].fxcnt]) {
257 case 1: /* Set speed */
258 UniEffect(UNI_S3MEFFECTA,inf);
259 break;
260 case 2: /* Volume slide */
261 if(inf) {
262 UniWriteByte(UNI_S3MEFFECTD);
263 if (inf>=0)
264 UniWriteByte((inf&0xf)<<4);
265 else
266 UniWriteByte((-inf)&0xf);
267 }
268 break;
269 /* effect 3, set channel volume, done in UnpackTrack */
270 case 4: /* Porta up/down */
271 if(inf) {
272 if(inf>0) {
273 UniEffect(UNI_S3MEFFECTE,inf);
274 fx4memory=UNI_S3MEFFECTE;
275 } else {
276 UniEffect(UNI_S3MEFFECTF,-inf);
277 fx4memory=UNI_S3MEFFECTF;
278 }
279 } else if(fx4memory)
280 UniEffect(fx4memory,0);
281 break;
282 /* effect 5, "Porta abs", not supported */
283 case 6: /* Porta to note */
284 UniEffect(UNI_ITEFFECTG,inf);
285 break;
286 case 7: /* Tremor */
287 UniEffect(UNI_S3MEFFECTI,inf);
288 break;
289 case 8: /* Arpeggio */
290 UniPTEffect(0x0,inf);
291 break;
292 case 9: /* Vibrato */
293 UniPTEffect(0x4,inf);
294 break;
295 case 0xa: /* Porta + Volume slide */
296 UniPTEffect(0x3,0);
297 if(inf) {
298 UniWriteByte(UNI_S3MEFFECTD);
299 if (inf>=0)
300 UniWriteByte((inf&0xf)<<4);
301 else
302 UniWriteByte((-inf)&0xf);
303 }
304 break;
305 case 0xb: /* Vibrato + Volume slide */
306 UniPTEffect(0x4,0);
307 if(inf) {
308 UniWriteByte(UNI_S3MEFFECTD);
309 if (inf>=0)
310 UniWriteByte((inf&0xf)<<4);
311 else
312 UniWriteByte((-inf)&0xf);
313 }
314 break;
315 case 0xc: /* Pattern break (in hex) */
316 UniPTEffect(0xd,inf);
317 break;
318 case 0xd: /* Pattern jump */
319 UniPTEffect(0xb,inf);
320 break;
321 /* effect 0xe, "Sync", not supported */
322 case 0xf: /* Retrig */
323 UniEffect(UNI_S3MEFFECTQ,inf&0xf);
324 break;
325 case 0x10: /* Sample offset */
326 UniPTEffect(0x9,inf);
327 break;
328 case 0x11: /* Fine volume slide */
329 if(inf) {
330 UniWriteByte(UNI_S3MEFFECTD);
331 if (inf>=0)
332 UniWriteByte((inf&0xf)<<4|0xf);
333 else
334 UniWriteByte(0xf0|((-inf)&0xf));
335 }
336 break;
337 case 0x12: /* Fine portamento */
338 if(inf) {
339 if(inf>0) {
340 UniEffect(UNI_S3MEFFECTE,0xf0|(inf&0xf));
341 fx4memory=UNI_S3MEFFECTE;
342 } else {
343 UniEffect(UNI_S3MEFFECTF,0xf0|((-inf)&0xf));
344 fx4memory=UNI_S3MEFFECTF;
345 }
346 } else if(fx4memory)
347 UniEffect(fx4memory,0);
348 break;
349 case 0x13: /* Delay note */
350 UniPTEffect(0xe,0xd0|(inf&0xf));
351 break;
352 case 0x14: /* Note cut */
353 UniPTEffect(0xc,0);
354 track[row].volume=0;
355 break;
356 case 0x15: /* Set tempo */
357 UniEffect(UNI_S3MEFFECTT,inf);
358 break;
359 case 0x16: /* Extra fine portamento */
360 if(inf) {
361 if(inf>0) {
362 UniEffect(UNI_S3MEFFECTE,0xe0|((inf>>2)&0xf));
363 fx4memory=UNI_S3MEFFECTE;
364 } else {
365 UniEffect(UNI_S3MEFFECTF,0xe0|(((-inf)>>2)&0xf));
366 fx4memory=UNI_S3MEFFECTF;
367 }
368 } else if(fx4memory)
369 UniEffect(fx4memory,0);
370 break;
371 case 0x17: /* Panning */
372 /* S3M pan, except offset by -64. */
373 if (inf>64)
374 UniEffect(UNI_ITEFFECTS0,0x91); /* surround */
375 else
376 UniPTEffect(0x8,(inf==64)?255:(inf+64)<<1);
377 of.flags |= UF_PANNING;
378 break;
379 }
380
381 }
382 if (track[row].volume) UniVolEffect(VOL_VOLUME,track[row].volume-1);
383 UniNewline();
384 }
385 return UniDup();
386}
387
388static int AMF_Load(int curious)
389{
390 int u,defaultpanning;
391 unsigned int t,realtrackcnt,realsmpcnt;
392 AMFSAMPLE s;
393 SAMPLE *q;
394 UWORD *track_remap;
395 ULONG samplepos, fileend;
396 UBYTE channel_remap[16];
397 int no_loopend;
398 (void)curious;
399 /* try to read module header */
400 _mm_read_UBYTES(mh->id,3,modreader);
401 mh->version =_mm_read_UBYTE(modreader);
402
403 /* For version 8, the song name is only 20 characters long and then come
404 // some data, which I do not know what is. The original code by Otto Chrons
405 // load the song name as 20 characters long and then it is overwritten again
406 // it another function, where it loads 32 characters, no matter which version
407 // it is. So we do the same here */
408 _mm_read_string(mh->songname,32,modreader);
409
410 mh->numsamples =_mm_read_UBYTE(modreader);
411 mh->numorders =_mm_read_UBYTE(modreader);
412 mh->numtracks =_mm_read_I_UWORD(modreader);
413
414 if(mh->version>=9)
415 mh->numchannels=_mm_read_UBYTE(modreader);
416 else
417 mh->numchannels=4;
418
419 if((!mh->numchannels)||(mh->numchannels>(mh->version>=12?32:16))) {
420 _mm_errno=MMERR_NOT_A_MODULE;
421 return 0;
422 }
423
424 if(mh->version>=11) {
425 memset(mh->panpos,0,32);
426 _mm_read_SBYTES(mh->panpos,(mh->version>=13)?32:16,modreader);
427 } else if(mh->version>=9)
428 _mm_read_UBYTES(channel_remap,16,modreader);
429
430 if (mh->version>=13) {
431 mh->songbpm=_mm_read_UBYTE(modreader);
432 if(mh->songbpm<32) {
433 _mm_errno=MMERR_NOT_A_MODULE;
434 return 0;
435 }
436 mh->songspd=_mm_read_UBYTE(modreader);
437 if(mh->songspd>32) {
438 _mm_errno=MMERR_NOT_A_MODULE;
439 return 0;
440 }
441 } else {
442 mh->songbpm=125;
443 mh->songspd=6;
444 }
445
446 if(_mm_eof(modreader)) {
447 _mm_errno = MMERR_LOADING_HEADER;
448 return 0;
449 }
450
451 /* set module variables */
452 of.initspeed = mh->songspd;
453 of.inittempo = mh->songbpm;
454 AMF_Version[AMFTEXTLEN-3]='0'+(mh->version/10);
455 AMF_Version[AMFTEXTLEN-1]='0'+(mh->version%10);
456 of.modtype = MikMod_strdup(AMF_Version);
457 of.numchn = mh->numchannels;
458 of.numtrk = mh->numorders*mh->numchannels;
459 if (mh->numtracks>of.numtrk)
460 of.numtrk=mh->numtracks;
461 of.numtrk++; /* add room for extra, empty track */
462 of.songname = DupStr(mh->songname,32,1);
463 of.numpos = mh->numorders;
464 of.numpat = mh->numorders;
465 of.reppos = 0;
466 of.flags |= UF_S3MSLIDES;
467 /* XXX whenever possible, we should try to determine the original format.
468 Here we assume it was S3M-style wrt bpmlimit... */
469 of.bpmlimit = 32;
470
471 /*
472 * Play with the panning table. Although the AMF format embeds a
473 * panning table, if the module was a MOD or an S3M with default
474 * panning and didn't use any panning commands, don't flag
475 * UF_PANNING, to use our preferred panning table for this case.
476 */
477 defaultpanning = 1;
478
479 if(mh->version>=11) {
480 for (t = 0; t < 32; t++) {
481 if (mh->panpos[t] > 64) {
482 of.panning[t] = PAN_SURROUND;
483 defaultpanning = 0;
484 } else
485 if (mh->panpos[t] == 64)
486 of.panning[t] = PAN_RIGHT;
487 else
488 of.panning[t] = (mh->panpos[t] + 64) << 1;
489 }
490 }
491 else
492 defaultpanning = 0;
493
494 if (defaultpanning) {
495 for (t = 0; t < of.numchn; t++)
496 if (of.panning[t] == (((t + 1) & 2) ? PAN_RIGHT : PAN_LEFT)) {
497 defaultpanning = 0; /* not MOD canonical panning */
498 break;
499 }
500 }
501 if (defaultpanning)
502 of.flags |= UF_PANNING;
503
504 of.numins=of.numsmp=mh->numsamples;
505
506 if(!AllocPositions(of.numpos)) return 0;
507 for(t=0;t<of.numpos;t++)
508 of.positions[t]=t;
509
510 if(!AllocTracks()) return 0;
511 if(!AllocPatterns()) return 0;
512
513 /* read AMF order table */
514 for (t=0;t<of.numpat;t++) {
515 if (mh->version>=14)
516 /* track size */
517 of.pattrows[t]=_mm_read_I_UWORD(modreader);
518 if ((mh->version==9) || (mh->version==10)) {
519 /* Only version 9 and 10 uses channel remap */
520 for (u = 0; u < of.numchn; u++)
521 of.patterns[t * of.numchn + channel_remap[u]] = _mm_read_I_UWORD(modreader);
522 }
523 else
524 _mm_read_I_UWORDS(of.patterns + (t * of.numchn), of.numchn, modreader);
525 }
526 if(_mm_eof(modreader)) {
527 _mm_errno = MMERR_LOADING_HEADER;
528 return 0;
529 }
530
531 /* read sample information */
532 if(!AllocSamples()) return 0;
533
534 no_loopend = 0;
535 if(mh->version == 10) {
536 no_loopend = AMF_ScanV10Instruments(modreader, of.numins);
537 }
538
539 q=of.samples;
540 for(t=0;t<of.numins;t++) {
541 /* try to read sample info */
542 s.type=_mm_read_UBYTE(modreader);
543 _mm_read_string(s.samplename,32,modreader);
544 _mm_read_string(s.filename,13,modreader);
545 s.offset =_mm_read_I_ULONG(modreader);
546
547 if(mh->version>=10)
548 s.length =_mm_read_I_ULONG(modreader);
549 else
550 s.length = _mm_read_I_UWORD(modreader);
551
552 s.c2spd =_mm_read_I_UWORD(modreader);
553 if(s.c2spd==8368) s.c2spd=8363;
554 s.volume =_mm_read_UBYTE(modreader);
555 /* "the tribal zone.amf" and "the way its gonna b.amf" by Maelcum
556 * are the only version 10 files I can find, and they have 32 bit
557 * reppos and repend, not 16. */
558 if(mh->version>=10 && no_loopend==0) {/* was 11 */
559 s.reppos =_mm_read_I_ULONG(modreader);
560 s.repend =_mm_read_I_ULONG(modreader);
561 } else if(mh->version==10) {
562 /* Early AMF 1.0 modules have the upper two bytes of
563 * the loop start and the entire loop end truncated.
564 * libxmp cites "sweetdrm.amf" and "facing_n.amf", but
565 * these are currently missing. M2AMF 1.3 (from DMP 2.32)
566 * has been confirmed to output these, however. */
567 s.reppos =_mm_read_I_UWORD(modreader);
568 s.repend =s.length;
569 /* There's not really a correct way to handle the loop
570 * end, but this makes unlooped samples work at least. */
571 if(s.reppos==0)
572 s.repend=0;
573 } else {
574 s.reppos =_mm_read_I_UWORD(modreader);
575 s.repend =_mm_read_I_UWORD(modreader);
576 if (s.repend==0xffff)
577 s.repend=0;
578 }
579
580 if(_mm_eof(modreader)) {
581 _mm_errno = MMERR_LOADING_SAMPLEINFO;
582 return 0;
583 }
584
585 q->samplename = DupStr(s.samplename,32,1);
586 q->speed = s.c2spd;
587 q->volume = s.volume;
588 if (s.type) {
589 q->seekpos = s.offset;
590 q->length = s.length;
591 q->loopstart = s.reppos;
592 q->loopend = s.repend;
593 if((s.repend-s.reppos)>2) q->flags |= SF_LOOP;
594 }
595 q++;
596 }
597
598 /* read track table */
599 if(!(track_remap=(UWORD*)MikMod_calloc(mh->numtracks+1,sizeof(UWORD))))
600 return 0;
601 _mm_read_I_UWORDS(track_remap+1,mh->numtracks,modreader);
602 if(_mm_eof(modreader)) {
603 MikMod_free(track_remap);
604 _mm_errno=MMERR_LOADING_TRACK;
605 return 0;
606 }
607
608 for(realtrackcnt=t=0;t<=mh->numtracks;t++)
609 if (realtrackcnt<track_remap[t])
610 realtrackcnt=track_remap[t];
611 if (realtrackcnt > (int)mh->numtracks) {
612 MikMod_free(track_remap);
613 _mm_errno=MMERR_NOT_A_MODULE;
614 return 0;
615 }
616 for(t=0;t<of.numpat*of.numchn;t++)
617 of.patterns[t]=(of.patterns[t]<=mh->numtracks)?
618 track_remap[of.patterns[t]]-1:(int)realtrackcnt;
619
620 MikMod_free(track_remap);
621
622 /* unpack tracks */
623 for(t=0;t<realtrackcnt;t++) {
624 if(_mm_eof(modreader)) {
625 _mm_errno = MMERR_LOADING_TRACK;
626 return 0;
627 }
628 if (!AMF_UnpackTrack(modreader)) {
629 _mm_errno = MMERR_LOADING_TRACK;
630 return 0;
631 }
632 if(!(of.tracks[t]=AMF_ConvertTrack()))
633 return 0;
634 }
635 /* add an extra void track */
636 UniReset();
637 for(t=0;t<64;t++) UniNewline();
638 of.tracks[realtrackcnt++]=UniDup();
639 for(t=realtrackcnt;t<of.numtrk;t++) of.tracks[t]=NULL;
640
641 /* compute sample offsets */
642 if(_mm_eof(modreader)) goto fail;
643 samplepos=_mm_ftell(modreader);
644 _mm_fseek(modreader,0,SEEK_END);
645 fileend=_mm_ftell(modreader);
646 _mm_fseek(modreader,samplepos,SEEK_SET);
647 for(realsmpcnt=t=0;t<of.numsmp;t++)
648 if(realsmpcnt<of.samples[t].seekpos)
649 realsmpcnt=of.samples[t].seekpos;
650 for(t=1;t<=realsmpcnt;t++) {
651 q=of.samples;
652 u=0;
653 while(q->seekpos!=t) {
654 if(++u==of.numsmp)
655 goto fail;
656 q++;
657 }
658 q->seekpos=samplepos;
659 samplepos+=q->length;
660 }
661 if(samplepos>fileend)
662 goto fail;
663
664 return 1;
665fail:
666 _mm_errno = MMERR_LOADING_SAMPLEINFO;
667 return 0;
668}
669
670static CHAR *AMF_LoadTitle(void)
671{
672 CHAR s[32];
673
674 _mm_fseek(modreader,4,SEEK_SET);
675 if(!_mm_read_UBYTES(s,32,modreader)) return NULL;
676
677 return(DupStr(s,32,1));
678}
679
680/*========== Loader information */
681
682MIKMODAPI MLOADER load_amf={
683 NULL,
684 "AMF",
685 "AMF (DSMI Advanced Module Format)",
686 AMF_Init,
687 AMF_Test,
688 AMF_Load,
689 AMF_Cleanup,
690 AMF_LoadTitle
691};
692
693/* ex:set ts=4: */