A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 725 lines 22 kB view raw
1#!/usr/bin/perl -s 2# __________ __ ___. 3# Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4# Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5# Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6# Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7# \/ \/ \/ \/ \/ 8# $Id$ 9# 10# Copyright (C) 2006 - 2008 by Daniel Stenberg 11# 12 13# See apps/language.c (TODO: Use common include for both) 14# Cookie and binary version for the binary lang file 15my $LANGUAGE_COOKIE = 0x1a; 16my $VOICE_COOKIE = 0x9a; 17my $LANGUAGE_VERSION = 0x06; 18my $LANGUAGE_FLAG_RTL = 0x01; 19my $LANGUAGE_FLAG_UNITS_FIRST = 0x02; 20 21my $HEADER_SIZE = 4; 22my $SUBHEADER_SIZE = 6; 23 24if(!$ARGV[0]) { 25 print <<MOO 26Usage: genlang [options] <langv2 file> 27 28 -p=<prefix> 29 Make the tool create a [prefix].c and [prefix].h file. 30 31 -b=<outfile> 32 Make the tool create a binary language (.lng) file named [outfile]. 33 The use of this option requires that you also use -e, -t and -i. 34 35 -c=<outfile> 36 Create binary voicestring file named [outfile]. Works like -b and can be 37 used the same time. 38 39 -e=<english lang file> 40 Point out the english (original source) file, to use that as master 41 language template. Always required. 42 43 -t=<target> 44 Specify which target you want the translations/phrases for. Required when 45 -b, -o, or -p is used. 46 47 The target can in fact be specified as numerous different strings, 48 separated with colons. This will make genlang to use all the specified 49 strings when searching for a matching phrase. 50 51 -i=<target id> 52 The target id number, needed for -b. 53 54 -o 55 Voice mode output. Outputs all id: and voice: lines for the given target! 56 57 -v 58 Enables verbose (debug) output. 59MOO 60; 61 exit; 62} 63 64my $prefix = $p; 65my $binary = $b; 66my $binvoice = $c; 67my $voiceout = $o; 68 69my $english = $e; 70if (!$english) { 71 print STDERR "Please specify the english lang source (with -e)!\n"; 72 exit; 73} 74 75my $target_id = $i; 76if($binary && !$target_id) { 77 print STDERR "Please specify a target id number (with -i)!\n"; 78 exit; 79} 80 81my $target = $t; 82if(!$target) { 83 print STDERR "Please specify a target (with -t)!\n"; 84 exit; 85} 86 87my $check = ($binary?.5:0) + ($prefix?1:0) + ($voiceout?1:0) + ($binvoice?.5:0); 88if($check > 1) { 89 print STDERR "Please use only one of -p, -o, -b, and -c\n"; 90 exit; 91} 92if(!$check) { 93 print STDERR "Please use at least one of -p, -o, -c, and -e\n"; 94 exit; 95} 96 97# Build up a regex which can be applied to target wildcard lists. We only need 98# to support prefix matches, so a target parameter of foo:bar can be expanded 99# to the regex "\*|f\*|fo\*|foo|b\*|ba\*|bar" and applied to the wildcard list 100# (plus end-of-string or commas on either side). The regex engine should 101# discard any duplicates generated for us in the process of constructing the 102# state machine, so we don't bother to check. 103my $target_regex = "(?:^|,) *(?:\\*"; 104my $model_regex = ""; # This matches the player model only! 105foreach my $target_part (split ':', $target) { 106 for (my $c=1; $c<=length $target_part; $c++) { 107 my $partial = substr $target_part, 0, $c; 108 $target_regex .= "|$partial\\*"; 109 } 110 $target_regex .= "|$target_part"; 111 $model_regex = $target_regex if (!$model_regex); 112} 113$target_regex .= ") *(?:,|\$)"; 114$target_regex = qr/$target_regex/; 115$model_regex .= ") *(?:,|\$)"; 116$model_regex = qr/$model_regex/; 117 118my $binpath = ""; 119if ($binary =~ m|(.*)/[^/]+|) { 120 $binpath = $1; 121} 122 123my $verbose=$v; 124 125my %id; # string to num hash 126my @idnum; # num to string array 127 128my %allphrases; # For sorting - an array of the <phrase> elements 129my %source; # id string to source phrase hash 130my %dest; # id string to dest phrase hash 131my %voice; # id string to voice phrase hash 132 133my %users = 134 ('core' => 0); 135 136my $input = $ARGV[0]; 137 138my @m; 139my $m="blank"; 140 141sub trim { 142 my ($string) = @_; 143 $string =~ s/^\s+//; 144 $string =~ s/\s+$//; 145 return $string; 146} 147 148sub blank { 149 # nothing to do 150} 151 152my %head; 153sub header { 154 my ($full, $n, $v)=@_; 155 $head{$n}=$v; 156} 157 158my %phrase; 159sub phrase { 160 my ($full, $n, $v)=@_; 161 $phrase{$n}=$v; 162} 163 164my %options; 165sub options { 166 my ($full, $n, $v)=@_; 167 $options{$n}=$v; 168} 169 170sub parsetarget { 171 my ($debug, $strref, $full, $n, $v)=@_; 172 my $string; 173 if ($n =~ $target_regex) { 174 $string = $v; 175 # Only override the previously set string if this is a device-specific match 176 $$strref = $string if (!$$strref || $$strref eq 'none' || $n =~ $model_regex); 177 return $string; 178 } 179} 180 181my $src; 182sub source { 183 parsetarget("src", \$src, @_); 184} 185 186my $dest; 187sub dest { 188 parsetarget("dest", \$dest, @_); 189} 190 191my $voice; 192sub voice { 193 parsetarget("voice", \$voice, @_); 194} 195 196sub file_is_newer { 197 my ($file1, $file2) = @_; 198 199 my @s1 = stat $file1; 200 my @s2 = stat $file2; 201 202 return 1 if ($s1[9] > $s2[9]); 203 return 0; 204} 205 206my %idmap; 207my %english; 208if($english) { 209 readenglish(); 210} 211 212sub readenglish { 213 # For the cases where the english file needs to be scanned/read, we must 214 # do it before we read the translated file. 215 216 my @idnum = ((0)); # start with a true number 217 my @vidnum = ((0x8000)); # first voice id 218 219 if ($binary and file_is_newer("$binpath/english.list", $english)) { 220 open(ENG, "<$binpath/english.list") || 221 die "Error: can't open $binpath/english.list"; 222 while (<ENG>) { 223 my ($user, $id, $value) = split ':', $_; 224 $idmap[$user]{$id} = $value; 225 $english{$id} = 1; 226 } 227 close ENG; 228 229 return; 230 } 231 232 open(ENG, "<$english") || die "Error: can't open $english"; 233 my @phrase; 234 my $id; 235 my $maybeid; 236 my $user; 237 my $withindest; 238 my $numphrases = 0; 239 my $numusers = 1; # core is already in the users map 240 241 while(<ENG>) { 242 # get rid of DOS newlines 243 $_ =~ tr/\r//d; 244 245 if($_ =~ /^ *\<phrase\>/) { 246 # this is the start of a phrase 247 } elsif($_ =~ /\<\/phrase\>/) { 248 # if id is something, when we count and store this phrase 249 if($id) { 250 # voice-only entries get a difference range 251 if($id =~ /^VOICE_/) { 252 # Assign an ID number to this entry 253 $idmap[$user]{$id}=$vidnum[$user]; 254 $vidnum[$user]++; 255 } else { 256 # Assign an ID number to this entry 257 $idmap[$user]{$id}=$idnum[$user]; 258 $idnum[$user]++; 259 # print STDERR "DEST: bumped idnum to $idnum[$user]\n"; 260 } 261 262 # this is the end of a phrase, add it to the english hash 263 $english{$id}=join("", @phrase); 264 } 265 undef @phrase; 266 $id=""; 267 } elsif($_ ne "\n") { 268 # gather everything related to this phrase 269 push @phrase, $_; 270 if($_ =~ /^ *\<dest\>/i) { 271 $withindest=1; 272 $deststr=""; 273 } elsif($withindest && ($_ =~ /^ *\<\/dest\>/i)) { 274 $withindest=0; 275 if($deststr && ($deststr !~ /^none\z/i)) { 276 # we unconditionally always use all IDs when the "update" 277 # feature is used 278 $id = $maybeid; 279 # print "DEST: use this id $id\n"; 280 } else { 281 # print "skip $maybeid for $name\n"; 282 } 283 } elsif($withindest && ($_ =~ / *([^:]+): *(.*)/)) { 284 my ($name, $val)=($1, $2); 285 $dest=""; # in case it is left untouched for when the 286 # model name isn't "ours" 287 dest($_, $name, $val); 288 289 if($dest) { 290 # Store the current dest string. If this target matches 291 # multiple strings, it will get updated several times. 292 $deststr = $dest; 293 } 294 } 295 } 296 297 if($_ =~ /^ *id: ([^ \t\n]+)/i) { 298 $maybeid=$1; 299 $sortorder{$maybeid}=$numphrases++; 300 } 301 if($_ =~ /^ *user: ([^ \t\n]+)/i) { 302 $user = $users{$1}; 303 if(!(defined $user)) { 304 $user = ++$numusers; 305 $users{$1} = $user; 306 } 307 } 308 } 309 close(ENG); 310} 311 312my @idcount; # counter for lang ID numbers 313my @voiceid; # counter for voice-only ID numbers 314 315for (keys %users) { 316 push @idcount, 0; 317 push @voiceid, 0x8001; 318} 319 320# 321# Now start the scanning of the selected language string 322# 323 324open(LANG, "<$input") || die "Error: couldn't read language file named $input\n"; 325my @phrase; 326my $langoptions = 0; 327 328while(<LANG>) { 329 330 $line++; 331 332 # get rid of DOS newlines 333 $_ =~ tr/\r//d; 334 335 if($_ =~ /^( *\#|[ \t\n\r]*\z)/) { 336 # comment or empty line - output it if it's part of the header 337 if ($_ =~ /LANGUAGE_IS_RTL/) { 338 $langoptions |= $LANGUAGE_FLAG_RTL; 339 } 340 if ($_ =~ /LANGUAGE_UNITS_FIRST/) { 341 $langoptions |= $LANGUAGE_FLAG_UNITS_FIRST; 342 } 343 next; 344 } 345 346 my $ll = $_; 347 348 # print "M: $m\n"; 349 350 push @phrase, $ll; 351 352 # this is an XML-lookalike tag 353 if (/^(<|[^\"<]+<)([^>]*)>/) { 354 my $part = $2; 355 # print "P: $part\n"; 356 357 if($part =~ /^\//) { 358 # this was a closing tag 359 360 if($part eq "/phrase") { 361 # closing the phrase 362 363 my $idstr = $phrase{'id'}; 364 my $idnum; 365 366 if($english && !$english{$idstr}) { 367 # print STDERR "$idstr doesn't exist for english, skip it\n"; 368 } elsif($dest =~ /^none\z/i || $src =~ /^none\z/i ) { 369 # "none" as dest (without quotes) means that this entire 370 # phrase is to be ignored 371 } elsif($sortfile) { 372 $allphrases{$idstr}=join('',@phrase); 373 } else { 374 # allow the keyword 'deprecated' to be used on dest and 375 # voice strings to mark that as deprecated. It will then 376 # be replaced with "". 377 378 $dest =~ s/^deprecate(|d)\z/\"\"/i; 379 $voice =~ s/^deprecate(|d)\z/\"\"/i; 380 381 # basic syntax error alerts, if there are no quotes we 382 # will assume an empty string was intended 383 if($dest !~ /^\"/) { 384 print STDERR "$input:$line:1: warning: dest before line lacks quotes ($dest)!\n"; 385 $dest='""'; 386 } 387 if($src !~ /^\"/) { 388 print STDERR "$input:$line:1: warning: source before line lacks quotes ($src)!\n"; 389 $src='""'; 390 } 391 if($voice !~ /^\"/ and $voice !~ /^none\z/i) { 392 print STDERR "$input:$line:1: warning: voice before line lacks quotes ($voice)!\n"; 393 $voice='""'; 394 } 395 if($dest eq '""' && $phrase{'desc'} !~ /deprecated/i && $idstr !~ /^VOICE/) { 396 print STDERR "$input:$line:1: warning: empty dest before line in non-deprecated phrase!\n"; 397 } 398 399 my $userstr = trim($phrase{'user'}); 400 my $user = $users{$userstr}; 401 if ($userstr eq "") { 402 print STDERR "$input:$line:1: warning: missing user!\n"; 403 $user = $users{"core"}; 404 } 405 elsif(!(defined $user)) { 406 if($english) { 407 print STDERR "$input:$line:1: warning: user was not found in $english!\n"; 408 $user = keys %users; # set to an invalid user so it won't be added 409 } else { 410 # we found a new user, add it to the usermap 411 $user = ++$numusers; 412 $users{$userstr} = $user; 413 } 414 } 415 416 # Use the ID name to figure out which id number range we 417 # should use for this phrase. Voice-only strings are 418 # separated. 419 420 if($idstr =~ /^VOICE/) { 421 $idnum = $voiceid[$user]++; 422 } else { 423 $idnum = $idcount[$user]++; 424 } 425 426 # Strip out the magic "Same as english" flag 427 $dest =~ s/^("?)~+/$1/; 428 $voice =~ s/^("?)~+/$1/; 429 430 $id{$idstr} = $idnum; 431 $idnum[$user][$idnum]=$idstr; 432 433 $source{$idstr}=$src; 434 $dest{$idstr}=$dest; 435 $voice{$idstr}=$voice; 436 437 if($verbose) { 438 print "id: $phrase{id} ($idnum)\n"; 439 print "source: $src\n"; 440 print "dest: $dest\n"; 441 print "voice: $voice\n"; 442 print "user: $user\n"; 443 } 444 445 undef $src; 446 undef $dest; 447 undef $voice; 448 undef $user; 449 undef %phrase; 450 } 451 undef @phrase; 452 } # end of </phrase> 453 454 # starts with a slash, this _ends_ this section 455 $m = pop @m; # get back old value, the previous level's tag 456 next; 457 } # end of tag close 458 459 # This is an opening (sub) tag 460 461 push @m, $m; # store old value 462 $m = $part; 463 next; 464 } 465 466 if(/^ *([^:]+): *(.*)/) { 467 my ($name, $val)=($1, $2); 468 &$m($_, $name, $val); 469 } 470} 471close(LANG); 472 473if ($sortfile) { 474 for(sort { $sortorder{$a} <=> $sortorder{$b} } keys %allphrases) { 475 print $allphrases{$_}; 476 } 477} 478 479if($prefix) { 480 # We create a .c and .h file 481 482 open(HFILE_CORE, ">$prefix/lang.h") || 483 die "Error: couldn't create file $prefix/lang.h\n"; 484 open(CFILE_CORE, ">$prefix/lang_core.c") || 485 die "Error: couldn't create file $prefix/lang_core.c\n"; 486 487 # get header file name 488 $headername = "$prefix/lang.h"; 489 $headername =~ s/(.*\/)*//; 490 491 print HFILE_CORE <<MOO 492/* This file was automatically generated using genlang */ 493/* 494 * The str() macro/functions is how to access strings that might be 495 * translated. Use it like str(MACRO) and expect a string to be 496 * returned! 497 */ 498#define str(x) language_strings[x] 499 500/* this is the array for holding the string pointers. 501 It will be initialized at runtime. */ 502extern unsigned char *language_strings[]; 503/* this contains the concatenation of all strings, separated by \\0 chars */ 504extern const unsigned char core_language_builtin[]; 505 506#include "${prefix}_enum.h" 507 508MOO 509 ; 510 511 close(HFILE_CORE); 512 513 open(HFILE_CORE, ">${prefix}_enum.h") || 514 die "couldn't create file ${prefix}_enum.h\n"; 515 516 print HFILE_CORE <<MOO 517/* This file was automatically generated using genlang */ 518#ifndef _LANG_ENUM_H_ 519#define _LANG_ENUM_H_ 520/* The enum below contains all available strings */ 521enum \{ 522MOO 523 ; 524 525 print CFILE_CORE <<MOO 526/* This file was automatically generated using genlang, the strings come 527 from "$input" */ 528 529#include "$headername" 530 531unsigned char *language_strings[LANG_LAST_INDEX_IN_ARRAY]; 532const unsigned char core_language_builtin[] = 533MOO 534; 535 536 # Output the ID names for the enum in the header file 537 my $i; 538 for $i (0 .. $idcount[$users{"core"}]-1) { 539 my $name=$idnum[$users{"core"}][$i]; # get the ID name 540 541 $name =~ tr/\"//d; # cut off the quotes 542 543 printf HFILE_CORE (" %s, /* %d */\n", $name, $i); 544 } 545 546# Output separation marker for last string ID and the upcoming voice IDs 547 548 print HFILE_CORE <<MOO 549 LANG_LAST_INDEX_IN_ARRAY, /* this is not a string, this is a marker */ 550 /* --- below this follows voice-only strings --- */ 551 VOICEONLY_DELIMITER = 0x8000, 552MOO 553 ; 554 555# Output the ID names for the enum in the header file 556 for $i (0x8001 .. ($voiceid[$users{"core"}]-1)) { 557 my $name=$idnum[$users{"core"}][$i]; # get the ID name 558 559 $name =~ tr/\"//d; # cut off the quotes 560 561 printf HFILE_CORE (" %s, /* 0x%x */\n", $name, $i); 562 } 563 564 # Output end of lang_enum.h 565 print HFILE_CORE <<MOO 566 LANG_LAST_VOICEONLY_INDEX_IN_ARRAY /* this is not a string, this is a marker */ 567}; 568/* end of generated enum list */ 569#endif /* _LANG_ENUM_H_ */ 570MOO 571 ; 572 573 # Output the target phrases for the source file 574 for $i (0 .. $idcount[$users{"core"}]-1) { 575 my $name=$idnum[$users{"core"}][$i]; # get the ID 576 my $dest = $dest{$name}; # get the destination phrase 577 578 $dest =~ s:\"$:\\0\":; # insert a \0 before the second quote 579 580 if(!$dest) { 581 # this is just to be on the safe side 582 $dest = '"\0"'; 583 } 584 585 printf CFILE_CORE (" %s\n", $dest); 586 } 587 588# Output end of string chunk 589 print CFILE_CORE <<MOO 590; 591/* end of generated string list */ 592MOO 593; 594 595 close(HFILE_CORE); 596 close(CFILE_CORE); 597} # end of the c/h file generation 598elsif($binary || $binvoice) { 599 # Creation of a binary lang file was requested 600 601 # We must first scan the english file to get the correct order of the id 602 # numbers used there, as that is what sets the id order for all language 603 # files. The english file is scanned before the translated file was 604 # scanned. 605 606 if($binary) { 607 open(OUTF, ">$binary") or die "Error: Can't create $binary"; 608 binmode OUTF; 609 printf OUTF ("%c%c%c%c", $LANGUAGE_COOKIE, $LANGUAGE_VERSION, $target_id, 610 $langoptions); # magic lang file header 611 } 612 if($binvoice) { 613 open(OUTV, ">$binvoice") or die "Error: Can't create $binary"; 614 binmode OUTV; 615 printf OUTV ("%c%c%c%c", $VOICE_COOKIE, $LANGUAGE_VERSION, $target_id, 616 $langoptions); # magic lang file header 617 } 618 619 # output the number of strings for each user 620 my $foffset = $HEADER_SIZE + $SUBHEADER_SIZE * keys(%users); 621 for (keys %users) { 622 my $size; 623 for $n (0 .. $idcount[$_]-1) { 624 $size += length(trim($dest{$idnum[$_][$n]})) + 1; 625 } 626 if($binary) { 627 printf OUTF ("%c%c%c%c%c%c", ($idcount[$_] >> 8), ($idcount[$_] & 0xff), 628 ($size >> 8), ($size & 0xff), ($foffset >> 8), ($foffset & 0xff)); 629 } 630 if($binvoice) { 631 printf OUTV ("%c%c%c%c%c%c", ($idcount[$_] >> 8), ($idcount[$_] & 0xff), 632 ($size >> 8), ($size & 0xff), ($foffset >> 8), ($foffset & 0xff)); 633 } 634 $foffset += $size; 635 } 636 637 for (keys %users) { 638 # loop over the target phrases 639 # This loops over the strings in the translated language file order 640 my @ids = ((0 .. ($idcount[$_]-1))); 641 push @ids, (0x8001 .. ($voiceid[$_]-1)); 642 for $n (@ids) { 643 my $name=$idnum[$_][$n]; # get the ID 644 my $dest = $dest{$name}; # get the destination phrase 645 my $voice = $voice{$name}; # get the destination voice string 646 647 if($dest && $n < 0x8000 && $binary) { 648 $dest =~ s/^\"(.*)\"\s*$/$1/g; # cut off quotes 649 650 # Now, make sure we get the number from the english sort order: 651 $idnum = $idmap[$_]{$name}; 652 653 printf OUTF ("%c%c%s\x00", ($idnum>>8), ($idnum&0xff), $dest); 654 } 655 if($voice && $binvoice) { 656 $voice =~ s/^\"(.*)\"\s*$/$1/g; # cut off quotes 657 # Now, make sure we get the number from the english sort order: 658 $idnum = $idmap[$_]{$name}; 659 printf OUTV ("%c%c%s\x00", ($idnum>>8), ($idnum&0xff), $voice); 660 } 661 } 662 } 663 if($binary) { 664 close(OUTF); 665 } 666 if($binvoice) { 667 close(OUTV); 668 } 669} 670elsif($voiceout) { 671 # voice output requested, display id: and voice: strings in a v1-like 672 # fashion 673 674 my @engl; 675 676 for (keys %users) { 677 # loop over the target phrases 678 # This loops over the strings in the translated language file order 679 my @ids = ((0 .. ($idcount[$_]-1))); 680 push @ids, (0x8001 .. ($voiceid[$_]-1)); 681 for $n (@ids) { 682 my $name=$idnum[$_][$n]; # get the ID 683 my $dest = $dest{$name}; # get the destination phrase 684 my $voice = $voice{$name}; # get the destination voice string 685 686 if($voice) { 687 $voice =~ s/^\"(.*)\"\s*$/$1/g; # cut off quotes 688 # Now, make sure we get the number from the english sort order: 689 $idnum = $idmap[$_]{$name}; 690 $engl[$idnum] = "#$idnum ($n)\nid: $name\nvoice: \"$voice\"\n"; 691 } 692 } 693 } 694 # Print the list in the the English sort order 695 for (@engl) { 696 print $_; 697 } 698} 699 700 701if($verbose) { 702 my $num_str = 0; 703 704 for (keys %users) { 705 $num_str += $idcount[$_]; 706 } 707 708 printf("%d ID strings scanned\n", $num_str); 709 710 print "* head *\n"; 711 for(keys %head) { 712 printf "$_: %s\n", $head{$_}; 713 } 714} 715 716if ($binary and !file_is_newer("$binpath/english.list", $english)) { 717 open(ENGLIST, ">$binpath/english.list") || 718 die "Failed creating $binpath/english.list"; 719 for my $user (keys %users) { 720 for my $id (sort { $idmap[$user]{$a} <=> $idmap[$user]{$b} } keys %{$idmap[$user]}) { 721 print ENGLIST "$user:$id:$idmap[$user]{$id}\n"; 722 } 723 } 724 close ENGLIST; 725}