/* 1 on success, 0 on failure */ int open_track(char *fname) { filept=-1; httpdata_reset(&htd); if(MPG123_OK != mpg123_param(mh, MPG123_ICY_INTERVAL, 0, 0)) error1("Cannot (re)set ICY interval: %s", mpg123_strerror(mh)); if(!strcmp(fname, "-")) { filept = STDIN_FILENO; #ifdef WIN32 _setmode(STDIN_FILENO, _O_BINARY); #endif return open_track_fd(); } else if (!strncmp(fname, "http://", 7)) /* http stream */ { #if defined (WANT_WIN32_SOCKETS) if(param.streamdump != NULL) { fprintf(stderr, "\nWarning: win32 networking conflicts with stream dumping. Aborting the dump.\n"); dump_close(); } /*Use recv instead of stdio functions */ win32_net_replace(mh); filept = win32_net_http_open(fname, &htd); #else filept = http_open(fname, &htd); #endif network_sockets_used = 1; /* utf-8 encoded URLs might not work under Win32 */ /* now check if we got sth. and if we got sth. good */ if( (filept >= 0) && (htd.content_type.p != NULL) && !APPFLAG(MPG123APP_IGNORE_MIME) && !(debunk_mime(htd.content_type.p) & IS_FILE) ) { error1("Unknown mpeg MIME type %s - is it perhaps a playlist (use -@)?", htd.content_type.p == NULL ? "<nil>" : htd.content_type.p); error("If you know the stream is mpeg1/2 audio, then please report this as "PACKAGE_NAME" bug"); return 0; } if(filept < 0) { error1("Access to http resource %s failed.", fname); return 0; } if(MPG123_OK != mpg123_param(mh, MPG123_ICY_INTERVAL, htd.icy_interval, 0)) error1("Cannot set ICY interval: %s", mpg123_strerror(mh)); if(param.verbose > 1) fprintf(stderr, "Info: ICY interval %li\n", (long)htd.icy_interval); } if(param.icy_interval > 0) { if(MPG123_OK != mpg123_param(mh, MPG123_ICY_INTERVAL, param.icy_interval, 0)) error1("Cannot set ICY interval: %s", mpg123_strerror(mh)); if(param.verbose > 1) fprintf(stderr, "Info: Forced ICY interval %li\n", param.icy_interval); } debug("OK... going to finally open."); /* Now hook up the decoder on the opened stream or the file. */ if(network_sockets_used) { return open_track_fd(); } else if(mpg123_open(mh, fname) != MPG123_OK) { error2("Cannot open %s: %s", fname, mpg123_strerror(mh)); return 0; } debug("Track successfully opened."); fresh = TRUE; return 1; }
int main(int sys_argc, char ** sys_argv) { int result; char end_of_files = FALSE; long parr; char *fname; int libpar = 0; mpg123_pars *mp; #if !defined(WIN32) && !defined(GENERIC) struct timeval start_time; #endif aux_out = stdout; /* Need to initialize here because stdout is not a constant?! */ #if defined (WANT_WIN32_UNICODE) if(win32_cmdline_utf8(&argc, &argv) != 0) { error("Cannot convert command line to UTF8!"); safe_exit(76); } #else argv = sys_argv; argc = sys_argc; #endif #if defined (WANT_WIN32_SOCKETS) win32_net_init(); #endif /* Extract binary and path, take stuff before/after last / or \ . */ if((prgName = strrchr(argv[0], '/')) || (prgName = strrchr(argv[0], '\\'))) { /* There is some explicit path. */ prgName[0] = 0; /* End byte for path. */ prgName++; binpath = argv[0]; } else { prgName = argv[0]; /* No path separators there. */ binpath = NULL; /* No path at all. */ } /* Need to initialize mpg123 lib here for default parameter values. */ result = mpg123_init(); if(result != MPG123_OK) { error1("Cannot initialize mpg123 library: %s", mpg123_plain_strerror(result)); safe_exit(77); } cleanup_mpg123 = TRUE; mp = mpg123_new_pars(&result); /* This may get leaked on premature exit(), which is mainly a cosmetic issue... */ if(mp == NULL) { error1("Crap! Cannot get mpg123 parameters: %s", mpg123_plain_strerror(result)); safe_exit(78); } /* get default values */ mpg123_getpar(mp, MPG123_DOWN_SAMPLE, &parr, NULL); param.down_sample = (int) parr; mpg123_getpar(mp, MPG123_RVA, ¶m.rva, NULL); mpg123_getpar(mp, MPG123_DOWNSPEED, ¶m.halfspeed, NULL); mpg123_getpar(mp, MPG123_UPSPEED, ¶m.doublespeed, NULL); mpg123_getpar(mp, MPG123_OUTSCALE, ¶m.outscale, NULL); mpg123_getpar(mp, MPG123_FLAGS, &parr, NULL); mpg123_getpar(mp, MPG123_INDEX_SIZE, ¶m.index_size, NULL); param.flags = (int) parr; param.flags |= MPG123_SEEKBUFFER; /* Default on, for HTTP streams. */ mpg123_getpar(mp, MPG123_RESYNC_LIMIT, ¶m.resync_limit, NULL); mpg123_getpar(mp, MPG123_PREFRAMES, ¶m.preframes, NULL); #ifdef OS2 _wildcard(&argc,&argv); #endif while ((result = getlopt(argc, argv, opts))) switch (result) { case GLO_UNKNOWN: fprintf (stderr, "%s: Unknown option \"%s\".\n", prgName, loptarg); usage(1); case GLO_NOARG: fprintf (stderr, "%s: Missing argument for option \"%s\".\n", prgName, loptarg); usage(1); } /* Do this _after_ parameter parsing. */ check_locale(); /* Check/set locale; store if it uses UTF-8. */ if(param.list_cpu) { const char **all_dec = mpg123_decoders(); printf("Builtin decoders:"); while(*all_dec != NULL){ printf(" %s", *all_dec); ++all_dec; } printf("\n"); mpg123_delete_pars(mp); return 0; } if(param.test_cpu) { const char **all_dec = mpg123_supported_decoders(); printf("Supported decoders:"); while(*all_dec != NULL){ printf(" %s", *all_dec); ++all_dec; } printf("\n"); mpg123_delete_pars(mp); return 0; } if(param.gain != -1) { warning("The parameter -g is deprecated and may be removed in the future."); } if (loptind >= argc && !param.listname && !param.remote) usage(1); /* Init audio as early as possible. If there is the buffer process to be spawned, it shouldn't carry the mpg123_handle with it. */ bufferblock = mpg123_safe_buffer(); /* Can call that before mpg123_init(), it's stateless. */ if(init_output(&ao) < 0) { error("Failed to initialize output, goodbye."); mpg123_delete_pars(mp); return 99; /* It's safe here... nothing nasty happened yet. */ } have_output = TRUE; /* ========================================================================================================= */ /* Enterning the leaking zone... we start messing with stuff here that should be taken care of when leaving. */ /* Don't just exit() or return out... */ /* ========================================================================================================= */ httpdata_init(&htd); #if !defined(WIN32) && !defined(GENERIC) if (param.remote) { param.verbose = 0; param.quiet = 1; param.flags |= MPG123_QUIET; } #endif /* Set the frame parameters from command line options */ if(param.quiet) param.flags |= MPG123_QUIET; #ifdef OPT_3DNOW if(dnow != 0) param.cpu = (dnow == SET_3DNOW) ? "3dnow" : "i586"; #endif if(param.cpu != NULL && (!strcmp(param.cpu, "auto") || !strcmp(param.cpu, ""))) param.cpu = NULL; if(!( MPG123_OK == (result = mpg123_par(mp, MPG123_VERBOSE, param.verbose, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_FLAGS, param.flags, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_DOWN_SAMPLE, param.down_sample, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_RVA, param.rva, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_FORCE_RATE, param.force_rate, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_DOWNSPEED, param.halfspeed, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_UPSPEED, param.doublespeed, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_ICY_INTERVAL, 0, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_RESYNC_LIMIT, param.resync_limit, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_TIMEOUT, param.timeout, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_OUTSCALE, param.outscale, 0)) && ++libpar && MPG123_OK == (result = mpg123_par(mp, MPG123_PREFRAMES, param.preframes, 0)) )) { error2("Cannot set library parameter %i: %s", libpar, mpg123_plain_strerror(result)); safe_exit(45); } if (!(param.listentry < 0) && !param.quiet) print_title(stderr); /* do not pollute stdout! */ { long default_index; mpg123_getpar(mp, MPG123_INDEX_SIZE, &default_index, NULL); if( param.index_size != default_index && (result = mpg123_par(mp, MPG123_INDEX_SIZE, param.index_size, 0.)) != MPG123_OK ) error1("Setting of frame index size failed: %s", mpg123_plain_strerror(result)); } if(param.force_rate && param.down_sample) { error("Down sampling and fixed rate options not allowed together!"); safe_exit(1); } /* Now actually get an mpg123_handle. */ mh = mpg123_parnew(mp, param.cpu, &result); if(mh == NULL) { error1("Crap! Cannot get a mpg123 handle: %s", mpg123_plain_strerror(result)); safe_exit(77); } mpg123_delete_pars(mp); /* Don't need the parameters anymore ,they're in the handle now. */ /* Prepare stream dumping, possibly replacing mpg123 reader. */ if(dump_open(mh) != 0) safe_exit(78); /* Now either check caps myself or query buffer for that. */ audio_capabilities(ao, mh); load_equalizer(mh); #ifdef HAVE_SETPRIORITY if(param.aggressive) { /* tst */ int mypid = getpid(); setpriority(PRIO_PROCESS,mypid,-20); } #endif #if defined (HAVE_SCHED_SETSCHEDULER) && !defined (__CYGWIN__) && !defined (HAVE_WINDOWS_H) /* Cygwin --realtime seems to fail when accessing network, using win32 set priority instead */ /* MinGW may have pthread installed, we prefer win32API */ if (param.realtime) { /* Get real-time priority */ struct sched_param sp; fprintf(stderr,"Getting real-time priority\n"); memset(&sp, 0, sizeof(struct sched_param)); sp.sched_priority = sched_get_priority_min(SCHED_FIFO); if (sched_setscheduler(0, SCHED_RR, &sp) == -1) fprintf(stderr,"Can't get real-time priority\n"); } #endif #ifdef HAVE_WINDOWS_H /* argument "3" is equivalent to realtime priority class */ win32_set_priority( param.realtime ? 3 : param.w32_priority); #endif if(!param.remote) prepare_playlist(argc, argv); #if !defined(WIN32) && !defined(GENERIC) /* Remote mode is special... but normal console and terminal-controlled operation needs to catch the SIGINT. For one it serves for track skip when not in terminal control mode. The more important use being a graceful exit, including telling the buffer process what's going on. */ if(!param.remote) catchsignal (SIGINT, catch_interrupt); #endif if(param.remote) { int ret; ret = control_generic(mh); safe_exit(ret); } #ifdef HAVE_TERMIOS debug1("param.term_ctrl: %i", param.term_ctrl); if(param.term_ctrl) term_init(); #endif if(APPFLAG(MPG123APP_CONTINUE)) frames_left = param.frame_number; while ((fname = get_next_file())) { char *dirname, *filename; int newdir; /* skip_tracks includes the previous one. */ if(skip_tracks) --skip_tracks; if(skip_tracks) { debug("Skipping this track."); continue; } if(param.delay > 0) { /* One should enable terminal control during that sleeping phase! */ if(param.verbose > 2) fprintf(stderr, "Note: pausing %i seconds before next track.\n", param.delay); output_pause(ao); #ifdef WIN32 Sleep(param.delay*1000); #else sleep(param.delay); #endif output_unpause(ao); } if(!APPFLAG(MPG123APP_CONTINUE)) frames_left = param.frame_number; debug1("Going to play %s", strcmp(fname, "-") ? fname : "standard input"); if(intflag || !open_track(fname)) { #ifdef HAVE_TERMIOS /* We need the opportunity to cancel in case of --loop -1 . */ if(param.term_ctrl) term_control(mh, ao); #endif /* No wait for a second interrupt before we started playing. */ if(intflag) break; continue; } if(!param.quiet) fprintf(stderr, "\n"); if(param.index) { if(param.verbose) fprintf(stderr, "indexing...\r"); mpg123_scan(mh); } /* Only trigger a seek if we do not want to start with the first frame. Rationale: Because of libmpg123 sample accuracy, this could cause an unnecessary backwards seek, that even may fail on non-seekable streams. For start frame of 0, we are already at the correct position! */ framenum = 0; if(param.start_frame > 0) framenum = mpg123_seek_frame(mh, param.start_frame, SEEK_SET); if(APPFLAG(MPG123APP_CONTINUE)) param.start_frame = 0; if(framenum < 0) { error1("Initial seek failed: %s", mpg123_strerror(mh)); if(mpg123_errcode(mh) == MPG123_BAD_OUTFORMAT) { fprintf(stderr, "%s", "So, you have trouble getting an output format... this is the matrix of currently possible formats:\n"); print_capabilities(ao, mh); fprintf(stderr, "%s", "Somehow the input data and your choices don't allow one of these.\n"); } mpg123_close(mh); continue; } /* Prinout and xterm title need this, possibly independently. */ newdir = split_dir_file(fname ? fname : "standard input", &dirname, &filename); if (!param.quiet) { if(newdir) fprintf(stderr, "Directory: %s\n", dirname); #ifdef HAVE_TERMIOS /* Reminder about terminal usage. */ if(param.term_ctrl) term_hint(); #endif fprintf(stderr, "Playing MPEG stream %lu of %lu: %s ...\n", (unsigned long)pl.pos, (unsigned long)pl.fill, filename); if(htd.icy_name.fill) fprintf(stderr, "ICY-NAME: %s\n", htd.icy_name.p); if(htd.icy_url.fill) fprintf(stderr, "ICY-URL: %s\n", htd.icy_url.p); } #if !defined(GENERIC) { const char *term_type; term_type = getenv("TERM"); if(term_type && param.xterm_title) { if(!strncmp(term_type,"xterm",5) || !strncmp(term_type,"rxvt",4)) fprintf(stderr, "\033]0;%s\007", filename); else if(!strncmp(term_type,"screen",6)) fprintf(stderr, "\033k%s\033\\", filename); else if(!strncmp(term_type,"iris-ansi",9)) fprintf(stderr, "\033P1.y %s\033\\\033P3.y%s\033\\", filename, filename); fflush(stderr); /* Paranoia: will the buffer buffer the escapes? */ } } #endif /* Rethink that SIGINT logic... */ #if !defined(WIN32) && !defined(GENERIC) #ifdef HAVE_TERMIOS if(!param.term_ctrl) #endif gettimeofday (&start_time, NULL); #endif while(!intflag) { int meta; if(param.frame_number > -1) { debug1("frames left: %li", (long) frames_left); if(!frames_left) { if(APPFLAG(MPG123APP_CONTINUE)) end_of_files = TRUE; break; } } if(!play_frame()) break; if(!param.quiet) { meta = mpg123_meta_check(mh); if(meta & (MPG123_NEW_ID3|MPG123_NEW_ICY)) { if(meta & MPG123_NEW_ID3) print_id3_tag(mh, param.long_id3, stderr); if(meta & MPG123_NEW_ICY) print_icy(mh, stderr); mpg123_meta_free(mh); /* Do not waste memory after delivering. */ } } if(!fresh && param.verbose) { #ifndef NOXFERMEM if (param.verbose > 1 || !(framenum & 0x7)) print_stat(mh,0,xfermem_get_usedspace(buffermem)); #else if(param.verbose > 1 || !(framenum & 0x7)) print_stat(mh,0,0); #endif } #ifdef HAVE_TERMIOS if(!param.term_ctrl) continue; else term_control(mh, ao); #endif } if(!param.smooth && param.usebuffer) buffer_drain(); if(param.verbose) print_stat(mh,0,xfermem_get_usedspace(buffermem)); if(!param.quiet) { double secs; long frank; fprintf(stderr, "\n"); if(mpg123_getstate(mh, MPG123_FRANKENSTEIN, &frank, NULL) == MPG123_OK && frank) fprintf(stderr, "This was a Frankenstein track.\n"); mpg123_position(mh, 0, 0, NULL, NULL, &secs, NULL); fprintf(stderr,"[%d:%02d] Decoding of %s finished.\n", (int)(secs / 60), ((int)secs) % 60, filename); } else if(param.verbose) fprintf(stderr, "\n"); mpg123_close(mh); if (intflag) { if(!skip_or_die(&start_time)) break; intflag = FALSE; #ifndef NOXFERMEM if(!param.smooth && param.usebuffer) buffer_resync(); #endif } if(end_of_files) break; } /* end of loop over input files */ /* Ensure we played everything. */ if(param.smooth && param.usebuffer) { buffer_drain(); buffer_resync(); } if(APPFLAG(MPG123APP_CONTINUE)) { continue_msg("CONTINUE"); } /* Free up memory used by playlist */ if(!param.remote) free_playlist(); safe_exit(0); /* That closes output and restores terminal, too. */ return 0; }
/* Print tags... limiting the UTF-8 to ASCII, if necessary. */ void print_id3_tag(mpg123_handle *mh, int long_id3, FILE *out) { char genre_from_v1 = 0; enum tagcode ti; mpg123_string tag[FIELDS]; size_t len[FIELDS]; mpg123_id3v1 *v1; mpg123_id3v2 *v2; /* no memory allocated here, so return is safe */ for(ti=0; ti<FIELDS; ++ti){ len[ti]=0; mpg123_init_string(&tag[ti]); } /* extract the data */ mpg123_id3(mh, &v1, &v2); /* Only work if something there... */ if(v1 == NULL && v2 == NULL) return; if(v2 != NULL) /* fill from ID3v2 data */ { len[TITLE] = transform(&tag[TITLE], v2->title); len[ARTIST] = transform(&tag[ARTIST], v2->artist); len[ALBUM] = transform(&tag[ALBUM], v2->album); len[COMMENT] = transform(&tag[COMMENT], v2->comment); len[YEAR] = transform(&tag[YEAR], v2->year); len[GENRE] = transform(&tag[GENRE], v2->genre); } if(v1 != NULL) /* fill gaps with ID3v1 data */ { /* I _could_ skip the recalculation of fill ... */ id3_gap(&tag[TITLE], 30, v1->title, &len[TITLE]); id3_gap(&tag[ARTIST], 30, v1->artist, &len[ARTIST]); id3_gap(&tag[ALBUM], 30, v1->album, &len[ALBUM]); id3_gap(&tag[COMMENT], 30, v1->comment, &len[COMMENT]); id3_gap(&tag[YEAR], 4, v1->year, &len[YEAR]); /* genre is special... v1->genre holds an index, id3v2 genre may contain indices in textual form and raw textual genres... */ if(!tag[GENRE].fill) { if(tag[GENRE].size >= 31 || mpg123_resize_string(&tag[GENRE],31)) { if(v1->genre <= genre_count) { strncpy(tag[GENRE].p, genre_table[v1->genre], 30); } else { strncpy(tag[GENRE].p,"Unknown",30); } tag[GENRE].p[30] = 0; /* V1 was plain ASCII, so strlen is fine. */ len[GENRE] = strlen(tag[GENRE].p); tag[GENRE].fill = len[GENRE] + 1; genre_from_v1 = 1; } } } if(tag[GENRE].fill && !genre_from_v1) { /* id3v2.3 says (id)(id)blabla and in case you want ot have (blabla) write ((blabla) also, there is (RX) Remix (CR) Cover id3v2.4 says "one or several of the ID3v1 types as numerical strings" or define your own (write strings), RX and CR Now I am very sure that I'll encounter hellishly mixed up id3v2 frames, so try to parse both at once. */ mpg123_string tmp; mpg123_init_string(&tmp); debug1("interpreting genre: %s\n", tag[GENRE].p); if(mpg123_copy_string(&tag[GENRE], &tmp)) { size_t num = 0; size_t nonum = 0; size_t i; enum { nothing, number, outtahere } state = nothing; tag[GENRE].fill = 0; /* going to be refilled */ /* number\n -> id3v1 genre */ /* (number) -> id3v1 genre */ /* (( -> ( */ for(i = 0; i < tmp.fill; ++i) { debug1("i=%lu", (unsigned long) i); switch(state) { case nothing: nonum = i; if(tmp.p[i] == '(') { num = i+1; /* number starting as next? */ state = number; debug1("( before number at %lu?", (unsigned long) num); } /* you know an encoding where this doesn't work? */ else if(tmp.p[i] >= '0' && tmp.p[i] <= '9') { num = i; state = number; debug1("direct number at %lu", (unsigned long) num); } else state = outtahere; break; case number: /* fake number alert: (( -> ( */ if(tmp.p[i] == '(') { nonum = i; state = outtahere; debug("no, it was (("); } else if(tmp.p[i] == ')' || tmp.p[i] == '\n' || tmp.p[i] == 0) { if(i-num > 0) { /* we really have a number */ int gid; char* genre = "Unknown"; tmp.p[i] = 0; gid = atoi(tmp.p+num); /* get that genre */ if(gid >= 0 && gid <= genre_count) genre = genre_table[gid]; debug1("found genre: %s", genre); if(tag[GENRE].fill) mpg123_add_string(&tag[GENRE], ", "); mpg123_add_string(&tag[GENRE], genre); nonum = i+1; /* next possible stuff */ state = nothing; debug1("had a number: %i", gid); } else { /* wasn't a number, nonum is set */ state = outtahere; debug("no (num) thing..."); } } else if(!(tmp.p[i] >= '0' && tmp.p[i] <= '9')) { /* no number at last... */ state = outtahere; debug("nothing numeric here"); } else { debug("still number..."); } break; default: break; } if(state == outtahere) break; } if(nonum < tmp.fill-1) { if(tag[GENRE].fill) mpg123_add_string(&tag[GENRE], ", "); mpg123_add_string(&tag[GENRE], tmp.p+nonum); } /* Do not like that ... assumes plain ASCII ... */ len[GENRE] = strlen(tag[GENRE].p); } mpg123_free_string(&tmp); } if(long_id3) { fprintf(out,"\n"); /* print id3v2 */ print_oneline(out, tag, TITLE, TRUE); print_oneline(out, tag, ARTIST, TRUE); print_oneline(out, tag, ALBUM, TRUE); print_oneline(out, tag, YEAR, TRUE); print_oneline(out, tag, GENRE, TRUE); print_oneline(out, tag, COMMENT, TRUE); fprintf(out,"\n"); } else { /* We are trying to be smart here and conserve some vertical space. So we will skip tags not set, and try to show them in two parallel columns if they are short, which is by far the most common case. */ int linelimit; /* Overhead is Name + ": " and also plus " " for right column. */ const int overhead[2] = { namelen[0]+2, namelen[1]+4 }; int climit[2]; /* Adapt formatting width to terminal if possible. */ linelimit = term_width(fileno(out)); if(linelimit < 0) linelimit=overhead[0]+30+overhead[1]+30; /* the old style, based on ID3v1 */ if(linelimit > 200) linelimit = 200; /* Not too wide. Also for format string safety. */ /* Divide the space between the two columns, not wasting any. */ climit[1] = linelimit/2-overhead[0]; climit[0] = linelimit-linelimit/2-overhead[1]; debug3("linelimits: %i < %i | %i >", linelimit, climit[0], climit[1]); if(climit[0] <= 0 || climit[1] <= 0) { /* Ensure disabled column printing, no play with signedness in comparisons. */ climit[0] = 0; climit[1] = 0; } fprintf(out,"\n"); /* Still use one separator line. Too ugly without. */ print_pair(out, climit, tag, len, TITLE, ARTIST); print_pair(out, climit, tag, len, COMMENT, ALBUM ); print_pair(out, climit, tag, len, YEAR, GENRE ); } for(ti=0; ti<FIELDS; ++ti) mpg123_free_string(&tag[ti]); if(v2 != NULL && APPFLAG(MPG123APP_LYRICS)) { /* find and print texts that have USLT IDs */ size_t i; for(i=0; i<v2->texts; ++i) { if(!memcmp(v2->text[i].id, "USLT", 4)) { /* split into lines, ensure usage of proper local line end */ size_t a=0; size_t b=0; char lang[4]; /* just a 3-letter ASCII code, no fancy encoding */ mpg123_string innline; mpg123_string outline; mpg123_string *uslt = &v2->text[i].text; memcpy(lang, &v2->text[i].lang, 3); lang[3] = 0; printf("Lyrics begin, language: %s; %s\n\n", lang, v2->text[i].description.fill ? v2->text[i].description.p : ""); mpg123_init_string(&innline); mpg123_init_string(&outline); while(a < uslt->fill) { b = a; while(b < uslt->fill && uslt->p[b] != '\n' && uslt->p[b] != '\r') ++b; /* Either found end of a line or end of the string (null byte) */ mpg123_set_substring(&innline, uslt->p, a, b-a); transform(&outline, &innline); printf(" %s\n", outline.p); if(uslt->p[b] == uslt->fill) break; /* nothing more */ /* Swallow CRLF */ if(uslt->fill-b > 1 && uslt->p[b] == '\r' && uslt->p[b+1] == '\n') ++b; a = b + 1; /* next line beginning */ } mpg123_free_string(&innline); mpg123_free_string(&outline); printf("\nLyrics end.\n"); } } } }