Browse Source

Plan 9 from Bell Labs 2004-12-24

David du Colombier 19 years ago
parent
commit
f1bc6f56a4
53 changed files with 6783 additions and 56 deletions
  1. 58 6
      dist/replica/_plan9.db
  2. 56 3
      dist/replica/plan9.db
  3. 56 0
      dist/replica/plan9.log
  4. 364 47
      sys/man/7/juke
  5. 156 0
      sys/man/7/playlistfs
  6. 8 0
      sys/src/cmd/auth/factotum/chap.c
  7. 12 0
      sys/src/games/music/Readme
  8. 13 0
      sys/src/games/music/debug.h
  9. BIN
      sys/src/games/music/icon/next.bit
  10. BIN
      sys/src/games/music/icon/pause.bit
  11. BIN
      sys/src/games/music/icon/play.bit
  12. BIN
      sys/src/games/music/icon/prev.bit
  13. BIN
      sys/src/games/music/icon/question.bit
  14. BIN
      sys/src/games/music/icon/root.bit
  15. BIN
      sys/src/games/music/icon/skull.bit
  16. BIN
      sys/src/games/music/icon/stop.bit
  17. BIN
      sys/src/games/music/icon/trash.bit
  18. 67 0
      sys/src/games/music/juke.rc
  19. 149 0
      sys/src/games/music/jukebox/client.c
  20. 10 0
      sys/src/games/music/jukebox/client.h
  21. 120 0
      sys/src/games/music/jukebox/colors.c
  22. 30 0
      sys/src/games/music/jukebox/colors.h
  23. 4 0
      sys/src/games/music/jukebox/mk.dep
  24. 16 0
      sys/src/games/music/jukebox/mkfile
  25. 1030 0
      sys/src/games/music/jukebox/music.c
  26. 289 0
      sys/src/games/music/jukebox/playlist.c
  27. 26 0
      sys/src/games/music/jukebox/playlist.h
  28. 152 0
      sys/src/games/music/jukefs/catset.c
  29. 8 0
      sys/src/games/music/jukefs/catset.h
  30. 757 0
      sys/src/games/music/jukefs/fs.c
  31. 6 0
      sys/src/games/music/jukefs/mk.dep
  32. 18 0
      sys/src/games/music/jukefs/mkfile
  33. 117 0
      sys/src/games/music/jukefs/object.h
  34. 614 0
      sys/src/games/music/jukefs/parse.c
  35. 15 0
      sys/src/games/music/jukefs/parse.h
  36. 489 0
      sys/src/games/music/jukefs/print.c
  37. 6 0
      sys/src/games/music/jukefs/print.h
  38. 44 0
      sys/src/games/music/jukefs/search.c
  39. 5 0
      sys/src/games/music/jukefs/search.h
  40. 204 0
      sys/src/games/music/jukefs/server.c
  41. 3 0
      sys/src/games/music/missing
  42. 54 0
      sys/src/games/music/mkfile
  43. 9 0
      sys/src/games/music/mkinc
  44. 81 0
      sys/src/games/music/playlistfs/boilerplate.c
  45. 901 0
      sys/src/games/music/playlistfs/fs.c
  46. 94 0
      sys/src/games/music/playlistfs/main.c
  47. 5 0
      sys/src/games/music/playlistfs/mk.dep
  48. 16 0
      sys/src/games/music/playlistfs/mkfile
  49. 413 0
      sys/src/games/music/playlistfs/player.c
  50. 150 0
      sys/src/games/music/playlistfs/playlist.h
  51. 20 0
      sys/src/games/music/playlistfs/playplumb.c
  52. 92 0
      sys/src/games/music/playlistfs/volume.c
  53. 46 0
      sys/src/games/music/readcd

+ 58 - 6
dist/replica/_plan9.db

@@ -1,8 +1,8 @@
 386 - 20000000775 sys sys 1010957353 0
 386/9load - 775 sys sys 1100269948 208708
-386/9loaddebug - 775 sys sys 1103607312 299841
+386/9loaddebug - 775 sys sys 1103775370 306612
 386/9loadlite - 775 sys sys 1100269949 132260
-386/9loadlitedebug - 775 sys sys 1102998039 194712
+386/9loadlitedebug - 775 sys sys 1103775371 195525
 386/9pc - 775 sys sys 1103607315 1833348
 386/9pc.gz - 664 sys sys 1077049336 635727
 386/9pccpu - 775 sys sys 1103607317 1488341
@@ -5073,8 +5073,9 @@ sys/man/7/INDEX - 664 sys sys 1041971551 89
 sys/man/7/INDEX.html - 664 sys sys 1019918566 806
 sys/man/7/astro - 664 sys sys 964455064 2454
 sys/man/7/dict - 664 sys sys 944959678 3438
-sys/man/7/juke - 664 sys sys 954266355 924
+sys/man/7/juke - 664 sys sys 1103794012 7769
 sys/man/7/map - 664 sys sys 1032632790 13549
+sys/man/7/playlistfs - 664 sys sys 1103794042 3831
 sys/man/7/scat - 664 sys sys 970069855 8904
 sys/man/8 - 20000000775 sys sys 1018384448 0
 sys/man/8/0intro - 664 sys sys 944959679 247
@@ -6860,7 +6861,7 @@ sys/src/cmd/auth/disable - 775 sys sys 1015008431 146
 sys/src/cmd/auth/enable - 775 sys sys 1015008430 134
 sys/src/cmd/auth/factotum - 20000000775 sys sys 1017165894 0
 sys/src/cmd/auth/factotum/apop.c - 664 sys sys 1071260312 6074
-sys/src/cmd/auth/factotum/chap.c - 664 sys sys 1102347052 8751
+sys/src/cmd/auth/factotum/chap.c - 664 sys sys 1103818824 8942
 sys/src/cmd/auth/factotum/confirm.c - 664 sys sys 1044829586 3103
 sys/src/cmd/auth/factotum/dat.h - 664 sys sys 1099176466 4809
 sys/src/cmd/auth/factotum/fgui.c - 664 sys sys 1044829587 15948
@@ -11293,6 +11294,58 @@ sys/src/games/mahjongg/level.c - 644 sys sys 1095792293 2522
 sys/src/games/mahjongg/mahjongg.c - 644 sys sys 1095792293 3452
 sys/src/games/mahjongg/mahjongg.h - 644 sys sys 1095792293 1606
 sys/src/games/mahjongg/mkfile - 644 sys sys 1095792293 230
+sys/src/games/music - 20000000775 sys sys 1103793915 0
+sys/src/games/music/Readme - 664 sys sys 1103793914 488
+sys/src/games/music/debug.h - 664 sys sys 1103793914 201
+sys/src/games/music/icon - 20000000775 sys sys 1103793917 0
+sys/src/games/music/icon/next.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/pause.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/play.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/prev.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/question.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/root.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/skull.bit - 644 sys sys 1103793917 143
+sys/src/games/music/icon/stop.bit - 644 sys sys 1103793917 143
+sys/src/games/music/icon/trash.bit - 644 sys sys 1103793917 143
+sys/src/games/music/juke.rc - 664 sys sys 1103793915 1352
+sys/src/games/music/jukebox - 20000000775 sys sys 1103794221 0
+sys/src/games/music/jukebox/client.c - 664 sys sys 1103793917 2747
+sys/src/games/music/jukebox/client.h - 664 sys sys 1103793917 248
+sys/src/games/music/jukebox/colors.c - 664 sys sys 1103793918 3819
+sys/src/games/music/jukebox/colors.h - 664 sys sys 1103793918 692
+sys/src/games/music/jukebox/mk.dep - 664 sys sys 1103793918 653
+sys/src/games/music/jukebox/mkfile - 664 sys sys 1103793918 234
+sys/src/games/music/jukebox/music.c - 664 sys sys 1103793919 28890
+sys/src/games/music/jukebox/playlist.c - 664 sys sys 1103793919 6456
+sys/src/games/music/jukebox/playlist.h - 664 sys sys 1103793919 505
+sys/src/games/music/jukefs - 20000000775 sys sys 1103793922 0
+sys/src/games/music/jukefs/catset.c - 664 sys sys 1103793919 3080
+sys/src/games/music/jukefs/catset.h - 664 sys sys 1103793919 251
+sys/src/games/music/jukefs/fs.c - 664 sys sys 1103793920 12708
+sys/src/games/music/jukefs/mk.dep - 664 sys sys 1103793920 759
+sys/src/games/music/jukefs/mkfile - 664 sys sys 1103793920 225
+sys/src/games/music/jukefs/object.h - 664 sys sys 1103793920 2068
+sys/src/games/music/jukefs/parse.c - 664 sys sys 1103793920 14098
+sys/src/games/music/jukefs/parse.h - 664 sys sys 1103793921 418
+sys/src/games/music/jukefs/print.c - 664 sys sys 1103793921 9016
+sys/src/games/music/jukefs/print.h - 664 sys sys 1103793921 174
+sys/src/games/music/jukefs/search.c - 664 sys sys 1103793921 1058
+sys/src/games/music/jukefs/search.h - 664 sys sys 1103793921 101
+sys/src/games/music/jukefs/server.c - 664 sys sys 1103793922 3521
+sys/src/games/music/missing - 775 sys sys 1103793915 72
+sys/src/games/music/mkfile - 664 sys sys 1103793915 723
+sys/src/games/music/mkinc - 664 sys sys 1103793915 92
+sys/src/games/music/playlistfs - 20000000775 sys sys 1103794221 0
+sys/src/games/music/playlistfs/boilerplate.c - 664 sys sys 1103793922 1087
+sys/src/games/music/playlistfs/fs.c - 664 sys sys 1103793922 18071
+sys/src/games/music/playlistfs/main.c - 664 sys sys 1103793922 1636
+sys/src/games/music/playlistfs/mk.dep - 664 sys sys 1103793923 594
+sys/src/games/music/playlistfs/mkfile - 664 sys sys 1103793923 187
+sys/src/games/music/playlistfs/player.c - 664 sys sys 1103793923 10228
+sys/src/games/music/playlistfs/playlist.h - 664 sys sys 1103793923 2608
+sys/src/games/music/playlistfs/playplumb.c - 664 sys sys 1103793924 280
+sys/src/games/music/playlistfs/volume.c - 664 sys sys 1103793924 2065
+sys/src/games/music/readcd - 775 sys sys 1103793915 996
 sys/src/games/sokoban - 20000000775 sys sys 1095792097 0
 sys/src/games/sokoban/README - 664 sys sys 1102439103 781
 sys/src/games/sokoban/graphics.c - 664 sys sys 1095792097 1846
@@ -12396,5 +12449,4 @@ usr/glenda/lib/profile - 664 glenda glenda 1021580005 847
 usr/glenda/readme.acme - 664 glenda glenda 1019860628 4753
 usr/glenda/readme.rio - 664 glenda glenda 1019860628 6370
 usr/glenda/tmp - 20000000775 glenda glenda 1018802620 0
-386/9loaddebug - 775 sys sys 1103775370 306612
-386/9loadlitedebug - 775 sys sys 1103775371 195525
+386/bin/auth/factotum - 775 sys sys 1103861395 313899

+ 56 - 3
dist/replica/plan9.db

@@ -53,7 +53,7 @@
 386/bin/auth/debug - 775 sys sys 1102171656 101029
 386/bin/auth/disable - 775 sys sys 1020319057 146
 386/bin/auth/enable - 775 sys sys 1020319057 134
-386/bin/auth/factotum - 775 sys sys 1103607303 313832
+386/bin/auth/factotum - 775 sys sys 1103861395 313899
 386/bin/auth/fgui - 775 sys sys 1102171657 217758
 386/bin/auth/guard.srv - 775 sys sys 1102171658 142815
 386/bin/auth/iam - 775 sys sys 1085076981 50791
@@ -5073,8 +5073,9 @@ sys/man/7/INDEX - 664 sys sys 1041971551 89
 sys/man/7/INDEX.html - 664 sys sys 1019918566 806
 sys/man/7/astro - 664 sys sys 964455064 2454
 sys/man/7/dict - 664 sys sys 944959678 3438
-sys/man/7/juke - 664 sys sys 954266355 924
+sys/man/7/juke - 664 sys sys 1103794012 7769
 sys/man/7/map - 664 sys sys 1032632790 13549
+sys/man/7/playlistfs - 664 sys sys 1103794042 3831
 sys/man/7/scat - 664 sys sys 970069855 8904
 sys/man/8 - 20000000775 sys sys 1018384448 0
 sys/man/8/0intro - 664 sys sys 944959679 247
@@ -6860,7 +6861,7 @@ sys/src/cmd/auth/disable - 775 sys sys 1015008431 146
 sys/src/cmd/auth/enable - 775 sys sys 1015008430 134
 sys/src/cmd/auth/factotum - 20000000775 sys sys 1017165894 0
 sys/src/cmd/auth/factotum/apop.c - 664 sys sys 1071260312 6074
-sys/src/cmd/auth/factotum/chap.c - 664 sys sys 1102347052 8751
+sys/src/cmd/auth/factotum/chap.c - 664 sys sys 1103818824 8942
 sys/src/cmd/auth/factotum/confirm.c - 664 sys sys 1044829586 3103
 sys/src/cmd/auth/factotum/dat.h - 664 sys sys 1099176466 4809
 sys/src/cmd/auth/factotum/fgui.c - 664 sys sys 1044829587 15948
@@ -11293,6 +11294,58 @@ sys/src/games/mahjongg/level.c - 644 sys sys 1095792293 2522
 sys/src/games/mahjongg/mahjongg.c - 644 sys sys 1095792293 3452
 sys/src/games/mahjongg/mahjongg.h - 644 sys sys 1095792293 1606
 sys/src/games/mahjongg/mkfile - 644 sys sys 1095792293 230
+sys/src/games/music - 20000000775 sys sys 1103793915 0
+sys/src/games/music/Readme - 664 sys sys 1103793914 488
+sys/src/games/music/debug.h - 664 sys sys 1103793914 201
+sys/src/games/music/icon - 20000000775 sys sys 1103793917 0
+sys/src/games/music/icon/next.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/pause.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/play.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/prev.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/question.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/root.bit - 644 sys sys 1103793916 143
+sys/src/games/music/icon/skull.bit - 644 sys sys 1103793917 143
+sys/src/games/music/icon/stop.bit - 644 sys sys 1103793917 143
+sys/src/games/music/icon/trash.bit - 644 sys sys 1103793917 143
+sys/src/games/music/juke.rc - 664 sys sys 1103793915 1352
+sys/src/games/music/jukebox - 20000000775 sys sys 1103794221 0
+sys/src/games/music/jukebox/client.c - 664 sys sys 1103793917 2747
+sys/src/games/music/jukebox/client.h - 664 sys sys 1103793917 248
+sys/src/games/music/jukebox/colors.c - 664 sys sys 1103793918 3819
+sys/src/games/music/jukebox/colors.h - 664 sys sys 1103793918 692
+sys/src/games/music/jukebox/mk.dep - 664 sys sys 1103793918 653
+sys/src/games/music/jukebox/mkfile - 664 sys sys 1103793918 234
+sys/src/games/music/jukebox/music.c - 664 sys sys 1103793919 28890
+sys/src/games/music/jukebox/playlist.c - 664 sys sys 1103793919 6456
+sys/src/games/music/jukebox/playlist.h - 664 sys sys 1103793919 505
+sys/src/games/music/jukefs - 20000000775 sys sys 1103793922 0
+sys/src/games/music/jukefs/catset.c - 664 sys sys 1103793919 3080
+sys/src/games/music/jukefs/catset.h - 664 sys sys 1103793919 251
+sys/src/games/music/jukefs/fs.c - 664 sys sys 1103793920 12708
+sys/src/games/music/jukefs/mk.dep - 664 sys sys 1103793920 759
+sys/src/games/music/jukefs/mkfile - 664 sys sys 1103793920 225
+sys/src/games/music/jukefs/object.h - 664 sys sys 1103793920 2068
+sys/src/games/music/jukefs/parse.c - 664 sys sys 1103793920 14098
+sys/src/games/music/jukefs/parse.h - 664 sys sys 1103793921 418
+sys/src/games/music/jukefs/print.c - 664 sys sys 1103793921 9016
+sys/src/games/music/jukefs/print.h - 664 sys sys 1103793921 174
+sys/src/games/music/jukefs/search.c - 664 sys sys 1103793921 1058
+sys/src/games/music/jukefs/search.h - 664 sys sys 1103793921 101
+sys/src/games/music/jukefs/server.c - 664 sys sys 1103793922 3521
+sys/src/games/music/missing - 775 sys sys 1103793915 72
+sys/src/games/music/mkfile - 664 sys sys 1103793915 723
+sys/src/games/music/mkinc - 664 sys sys 1103793915 92
+sys/src/games/music/playlistfs - 20000000775 sys sys 1103794221 0
+sys/src/games/music/playlistfs/boilerplate.c - 664 sys sys 1103793922 1087
+sys/src/games/music/playlistfs/fs.c - 664 sys sys 1103793922 18071
+sys/src/games/music/playlistfs/main.c - 664 sys sys 1103793922 1636
+sys/src/games/music/playlistfs/mk.dep - 664 sys sys 1103793923 594
+sys/src/games/music/playlistfs/mkfile - 664 sys sys 1103793923 187
+sys/src/games/music/playlistfs/player.c - 664 sys sys 1103793923 10228
+sys/src/games/music/playlistfs/playlist.h - 664 sys sys 1103793923 2608
+sys/src/games/music/playlistfs/playplumb.c - 664 sys sys 1103793924 280
+sys/src/games/music/playlistfs/volume.c - 664 sys sys 1103793924 2065
+sys/src/games/music/readcd - 775 sys sys 1103793915 996
 sys/src/games/sokoban - 20000000775 sys sys 1095792097 0
 sys/src/games/sokoban/README - 664 sys sys 1102439103 781
 sys/src/games/sokoban/graphics.c - 664 sys sys 1095792097 1846

+ 56 - 0
dist/replica/plan9.log

@@ -17703,3 +17703,59 @@
 1103689879 0 c 386/bin/tar - 775 sys sys 1103688550 83885
 1103776295 0 c 386/9loaddebug - 775 sys sys 1103775370 306612
 1103776295 1 c 386/9loadlitedebug - 775 sys sys 1103775371 195525
+1103794299 0 c sys/man/7/juke - 664 sys sys 1103794012 7769
+1103794299 1 a sys/man/7/playlistfs - 664 sys sys 1103794042 3831
+1103794299 2 a sys/src/games/music - 20000000775 sys sys 1103793915 0
+1103794299 3 a sys/src/games/music/Readme - 664 sys sys 1103793914 488
+1103794299 4 a sys/src/games/music/debug.h - 664 sys sys 1103793914 201
+1103794299 5 a sys/src/games/music/icon - 20000000775 sys sys 1103793917 0
+1103794299 6 a sys/src/games/music/icon/next.bit - 644 sys sys 1103793916 143
+1103794299 7 a sys/src/games/music/icon/pause.bit - 644 sys sys 1103793916 143
+1103794299 8 a sys/src/games/music/icon/play.bit - 644 sys sys 1103793916 143
+1103794299 9 a sys/src/games/music/icon/prev.bit - 644 sys sys 1103793916 143
+1103794299 10 a sys/src/games/music/icon/question.bit - 644 sys sys 1103793916 143
+1103794299 11 a sys/src/games/music/icon/root.bit - 644 sys sys 1103793916 143
+1103794299 12 a sys/src/games/music/icon/skull.bit - 644 sys sys 1103793917 143
+1103794299 13 a sys/src/games/music/icon/stop.bit - 644 sys sys 1103793917 143
+1103794299 14 a sys/src/games/music/icon/trash.bit - 644 sys sys 1103793917 143
+1103794299 15 a sys/src/games/music/juke.rc - 664 sys sys 1103793915 1352
+1103794299 16 a sys/src/games/music/jukebox - 20000000775 sys sys 1103794221 0
+1103794299 17 a sys/src/games/music/jukebox/client.c - 664 sys sys 1103793917 2747
+1103794299 18 a sys/src/games/music/jukebox/client.h - 664 sys sys 1103793917 248
+1103794299 19 a sys/src/games/music/jukebox/colors.c - 664 sys sys 1103793918 3819
+1103794299 20 a sys/src/games/music/jukebox/colors.h - 664 sys sys 1103793918 692
+1103794299 21 a sys/src/games/music/jukebox/mk.dep - 664 sys sys 1103793918 653
+1103794299 22 a sys/src/games/music/jukebox/mkfile - 664 sys sys 1103793918 234
+1103794299 23 a sys/src/games/music/jukebox/music.c - 664 sys sys 1103793919 28890
+1103794299 24 a sys/src/games/music/jukebox/playlist.c - 664 sys sys 1103793919 6456
+1103794299 25 a sys/src/games/music/jukebox/playlist.h - 664 sys sys 1103793919 505
+1103794299 26 a sys/src/games/music/jukefs - 20000000775 sys sys 1103793922 0
+1103794299 27 a sys/src/games/music/jukefs/catset.c - 664 sys sys 1103793919 3080
+1103794299 28 a sys/src/games/music/jukefs/catset.h - 664 sys sys 1103793919 251
+1103794299 29 a sys/src/games/music/jukefs/fs.c - 664 sys sys 1103793920 12708
+1103794299 30 a sys/src/games/music/jukefs/mk.dep - 664 sys sys 1103793920 759
+1103794299 31 a sys/src/games/music/jukefs/mkfile - 664 sys sys 1103793920 225
+1103794299 32 a sys/src/games/music/jukefs/object.h - 664 sys sys 1103793920 2068
+1103794299 33 a sys/src/games/music/jukefs/parse.c - 664 sys sys 1103793920 14098
+1103794299 34 a sys/src/games/music/jukefs/parse.h - 664 sys sys 1103793921 418
+1103794299 35 a sys/src/games/music/jukefs/print.c - 664 sys sys 1103793921 9016
+1103794299 36 a sys/src/games/music/jukefs/print.h - 664 sys sys 1103793921 174
+1103794299 37 a sys/src/games/music/jukefs/search.c - 664 sys sys 1103793921 1058
+1103794299 38 a sys/src/games/music/jukefs/search.h - 664 sys sys 1103793921 101
+1103794299 39 a sys/src/games/music/jukefs/server.c - 664 sys sys 1103793922 3521
+1103794299 40 a sys/src/games/music/missing - 775 sys sys 1103793915 72
+1103794299 41 a sys/src/games/music/mkfile - 664 sys sys 1103793915 723
+1103794299 42 a sys/src/games/music/mkinc - 664 sys sys 1103793915 92
+1103794299 43 a sys/src/games/music/playlistfs - 20000000775 sys sys 1103794221 0
+1103794299 44 a sys/src/games/music/playlistfs/boilerplate.c - 664 sys sys 1103793922 1087
+1103794299 45 a sys/src/games/music/playlistfs/fs.c - 664 sys sys 1103793922 18071
+1103794299 46 a sys/src/games/music/playlistfs/main.c - 664 sys sys 1103793922 1636
+1103794299 47 a sys/src/games/music/playlistfs/mk.dep - 664 sys sys 1103793923 594
+1103794299 48 a sys/src/games/music/playlistfs/mkfile - 664 sys sys 1103793923 187
+1103794299 49 a sys/src/games/music/playlistfs/player.c - 664 sys sys 1103793923 10228
+1103794299 50 a sys/src/games/music/playlistfs/playlist.h - 664 sys sys 1103793923 2608
+1103794299 51 a sys/src/games/music/playlistfs/playplumb.c - 664 sys sys 1103793924 280
+1103794299 52 a sys/src/games/music/playlistfs/volume.c - 664 sys sys 1103793924 2065
+1103794299 53 a sys/src/games/music/readcd - 775 sys sys 1103793915 996
+1103819503 0 c sys/src/cmd/auth/factotum/chap.c - 664 sys sys 1103818824 8942
+1103862710 0 c 386/bin/auth/factotum - 775 sys sys 1103861395 313899

+ 364 - 47
sys/man/7/juke

@@ -1,52 +1,369 @@
 .TH JUKE 7
 .SH NAME
-juke \-
-.SM CDROM
-juke box
+juke \- music jukebox
 .SH SYNOPSIS
-.B 9fs juke
+.B juke
+[
+.B \-t
+]
+[
+.B \-w
+]
+[
+.B \-h
+.I srvhost
+]
+[
+.B \-s
+.I srvname
+]
+.ift .sp 0.5
+.ifn .sp
+.B games/jukebox
+[
+.B \-t
+]
+[
+.B \-w
+]
+.ift .sp 0.5
+.ifn .sp
+.B games/jukefs
+[
+.B \-m
+.I mountpoint
+]
+[
+.B \-s
+.I srvname
+]
+[
+.I mapfile
+]
 .SH DESCRIPTION
-The
-.I juke
-file system is a stand-alone file server,
-.BR jukefs ,
-that stores copies of
-.SM CDROM\c
-s
-in a simulation of the true juke box that it replaces.
-Each `disc' in the juke box appears as a file in
-.B /n/juke
-or in a subdirectory of
-.BR /n/juke .
-Here are descriptions of some of them.
-.nr zz \w'\f(CWsupercomputing.93\fP'u/1n+2
-.TP \n(zz
-.B plan9.1992
-The 1992 Plan 9 release.
-.TP
-.B plan9.1995
-The 1995 Plan 9 release.
-.TP
-.B dss/dss.???
-Digitized Sky Survey (102 discs covering the night sky); access with
-.IR scat (7).
-.TP
-.B eg/*
-Chess end games.
-.PP
-To see the contents of a
-.SM CDROM\c
-, start
-.B 9660srv
+.I Jukebox
+controls a playlist server
 (see
-.IR dossrv (4))
-and mount the service with the file name of the
-.SM CDROM
-as the attach specifier.
-.SH BUGS
-There should be a way to access the contents of the
-.SM CDROM\c
-s
-without running
-.B 9660srv
-locally.
+.BR playlistsrv (7))
+through a graphical user interface.  It connects to a music database server which reads a set of
+.I map
+files that describe recordings and their location.  Currently, there is
+one set of maps, mostly for classical music, with some jazz and other stuff
+thrown in.  These are served by
+.BR jukefs ,
+which presents a file system conventionally mounted at
+.BR /mnt/juke .
+The playlist, explained below, is managed by a file system implemented by
+.IR playlistfs (7)
+and normally mounted on
+.BR /mnt .
+.PP
+.I Jukebox
+is most easily started through the
+.I juke
+shell script.
+.PP
+.I Jukebox
+has four windows, which can be selected by clicking the appropriate tab
+at the top of the window.
+.PP
+Above the tab are nine buttons and a volume slider.  The
+.ift buttons, shown below,
+.ifn buttons
+are named, from left to right,
+.IR Exit ,
+.IR Pause ,
+.IR Play ,
+.IR Halt ,
+.IR Back ,
+.IR Forward ,
+.IR Root ,
+.IR Delete ,
+and
+.IR Help .
+The buttons are
+.I active
+when they are displayed in dark green (or red).  When they are pale blue
+they are
+.IR inactive .
+The Exit button is always active; it exits the program (but leaves the playlist and music database
+servers running).
+.PP
+The
+.I browse
+window is for browsing through the music and selecting music to play.
+Browsing down in the music hierarchy is done by clicking button one on
+an item.  Clicking button three goes back up.
+Clicking button two recursively adds all files below the selected item to
+the
+.IR "play list" .
+.PP
+The selected music is displayed in the
+.I playlist 
+window.
+The track currently playing is shown in the
+.I playing
+window.
+.PP
+The
+.I Root
+button browses back to the root.
+.PP
+The
+.I Delete
+button empties the playlist.
+.PP
+The
+.I Help
+displays a minimal on-line manual.
+.PP
+.I Play
+starts playing at the beginning of the play list, or at the selected track in
+the play list.
+.PP
+During play,
+.IR Pause ,
+.IR Stop ,
+.IR Back ,
+and
+.I Forward
+are active.
+.I Back
+and
+.I Forward
+go back or forward a track at a time.  The other buttons do the obvious thing.
+.PP
+The
+.B \-t
+flag chooses a tiny font, useful for handhelds.
+.PP
+The
+.B \-w
+flag creates the jukebox in a new window.  Normally, the jukebox takes over
+the window in which it is invoked.
+.PP
+The
+.B \-s
+flag specifies the name under which the file descriptors of the playlist and databse servers are posted
+in /srv.  This allows two or more play list servers to exist on one platform, e.g., when
+there are several audio devices.  The default value of the flag is
+.B $\f2user\fP
+for a playlist server at
+.B /srv/playlistfs.$\f2user\fP
+and a database server at
+.BR /srv/jukefs.$\f2user\fP .
+.sp
+.LP
+.B Jukefs
+reads a set of
+.I maps
+describing the music data, builds an in-memory database, and provides
+lookup service to
+.IR jukebox .
+The default map is
+.BR /sys/lib/music/map .
+It consists of a hierarchical set of
+.IR objects .
+Each object has a type, a value, zero or more attribute-value
+pairs and zero or more subobjects.    An object consists of the
+type, followed by its contents between curly brackets.
+Attribute value pairs consist
+of a single line containing an attribute name, an equals sign, and
+a value.
+The value of an object is any text not containing curly brackets or equals
+signs.  Here is an example:
+.EX
+.ps -2
+.vs -2p
+.sp
+category {
+	composer = mahler
+
+	Gustav Mahler
+	(1860 — 1911)
+
+	work {
+		path {classic/mahler}
+		class = symphonic
+		orchestra = rfo
+		conductor = Waart,~Edo~de
+
+		Symphony Nº 5 in c♯ (RFO, Vienna)
+		performance{
+			Radio Filharmonisch Orkest Holland
+			Edo de Waart, conductor
+
+			recorded: Musikverein, Vienna, May 6, 1996
+		}
+		command {number}
+		track {
+			Trauermarsch (In gemessenem Schritt. Streng. Wie ein Kondukt)
+			time {13:55}
+			file {034.pac}
+		}
+		track {
+			Stürmisch bewegt, mit größter Vehemenz
+			time {15:34}
+			file {035.pac}
+		}
+		track {
+			Scherzo (Kräftig, nicht zu schnell)
+			time {18:54}
+			file {036.pac}
+		}
+		track {
+			Adagietto (Sehr Langsam)
+			time {10:01}
+			file {037.pac}
+		}
+		track {
+			Rondo–Finale (Allegro)
+			time {15:44}
+			file {038.pac}
+		}
+	}
+}
+.EE
+.LP
+This example shows a
+.I category
+object for the composer Gustav Mahler (the value consists of the two
+lines `Gustav Mahler' and `(1860 — 1911)') with one subobject, a
+.I work
+object whose value is `Symphony Nº 5 in c♯ (RFO, Vienna)'.  The work object
+contains six subobjects: one
+.I performance
+object and five
+.I track
+objects.
+.PP
+.I Category
+objects must contain exactly one attribute-value pair.  The attribute
+names a subobject of the root under which this category object will
+be placed.  Gustav Mahler, thus, will be placed in
+Root→composer.
+.IR Work ,
+.IR Recording ,
+.IR Part ,
+and
+.IR Track ,
+objects all describe named containers for subunits.
+A
+.IR Lyrics ,
+.IR Performance ,
+or
+.IR Soloists
+object adds information to a
+.IR Work ,
+.IR Recording ,
+.IR Part ,
+or
+.IR Track ,
+object.  It should only contain text.
+The same is true for a
+.I Time
+object; however, it should only be used adjacent to
+.I File
+objects and it should contain the running time of that file (this
+is for future use).
+.PP
+A
+.I File
+object specifies a file to be played.  When the
+.I Select
+button is pressed, all file objects contained hierarchically in the
+selected object are added to the playlist.
+.PP
+There are a number of pseudo objects:
+.I Command
+may contain either
+.I sort
+or
+.IR number .
+The
+.I sort
+command sorts the subobjects of the object it appears in by
+.I key
+or textual content.
+The
+.I number
+commands prepends numbers to the texts of its subobjects
+(e.g., for the parts in a symphony)
+.PP
+An
+.I Include
+object is replaced by the contents of the named file.
+.PP
+A
+.I Key
+object specifies a key for sorting subobjects.
+.PP
+Finally, a
+.I Path
+object specifies a path to be prepended to the files named in
+hierarchically contained
+.I File
+objects.
+.PP
+The attribute-value value pairs arrange for entries to be made of the
+current object in a
+.I Category
+object named by the attribute directly under the root.
+.sp
+.LP
+The interface to the browsing database is through a file system
+implemented by
+.BR jukefs .
+The file system synthesises a directory per object.  Each directory contains a set of files
+describing the object's attributes:
+.TP
+.B children
+contains a new-line separated list of subobject names.  For each name,
+.I x
+the directory
+.BI /mnt/juke/ x
+describes the subobject.
+.TP
+.B digest
+contains a one-line summary of the object
+.TP
+.B files
+is a new-line separated list of file objects contained in this object.
+Each line consists of object name and file name.
+.TP
+.B fulltext
+is the fulltextual value of the object.
+.TP
+.B key
+contains the key by which objects are sorted
+.TP
+.B miniparentage
+is a one-line summary of the objects and the path leading to it from the root.
+This is the line displayed in the playlist and bottom browse windows of
+.BR games/jukebox .
+.TP
+.B parent
+is the object reference to the parent of this object.
+.TP
+.B parentage
+is a full description of the path leading to this object and the object itself.
+This is the string displayed in the top of the Browse and Playing windows
+of
+.BR games/jukebox .
+.TP
+.B text
+is the text field of the object.
+.TP
+.B type
+is the type of the object
+.LP
+.SH FILES
+.BR /sys/lib/music/map :
+Default map file
+.BR /mnt/juke :
+Default mount point for the music database.
+.SH SOURCE
+.B /sys/src/games/music
+.SH SEE ALSO
+.IR playlistfs (7),
+.IR audio (7)

+ 156 - 0
sys/man/7/playlistfs

@@ -0,0 +1,156 @@
+.TH PLAYLISTFS 7
+.SH NAME
+playlistfs \- playlist file system
+.SH SYNOPSIS
+.B games/playlistfs
+[
+.B \-s
+.I postname
+]
+[
+.B \-m
+.I mountpoint
+]
+[
+.B \-a
+]
+.SH DESCRIPTION
+.B Playlistfs
+implements an audio player which plays files from a built-in play list.
+The player is controlled through three files, usually mounted at
+.BR /mnt .
+The files are
+.B /playctl
+for controlling play: start, stop, pause, skip, etc.;
+.B /playvol
+for controlling the playout volume; and
+.B /playlist
+for controlling the play list itself.
+.PP
+All three files can be written to control the player and read to obtain player
+status information.
+.PP
+When read, the files report the current status of the player, volume and playlist,
+respectively.  End of file is indicated by a read that returns zero bytes, as usual.
+However, in all three files, subsequent read operations will block until the status
+of the file changes and then report the changed state.  When the changed state has
+been read, another end-of-file indication is given, after which another read
+can be issued to wait for state changes.
+.PP
+The
+.B /playctl
+file returns strings of the form `\f2cmd n\fP'
+where
+.I cmd
+is one of
+.IR stop ,
+.IR pause ,
+or
+.I play
+and
+.I n
+is an index (or offset) into the playlist; indices start at zero.
+.PP
+The commands that can be written to
+.B /playctl
+take the same form; however, the index is an optional argument.  If the
+index is omitted, the current value is used. The commands are
+.IR play ,
+.IR stop ,
+.IR pause ,
+.IR resume ,
+and
+.IR skip .
+.I Play
+starts playing at the index.
+.I Stop
+stops playing.  If an index is given, the current index is set to it and
+can be used in future commands.
+.I Pause
+and
+.I Resume
+interrupt and continue play, respectively.  The index argument is always ignored and
+the whole command is ignored if the state in which they occur does not
+make sense.
+.I Skip
+adds the argument to the current index (adds one if no argument is given)
+and starts play at that index, stopping current play, if necessary.
+.PP
+Reads of
+.B /playvol
+return strings of the form
+.BR "`volume \f2n\fP'" ,
+where
+.I n
+is a number or, if there is more than one channel, a quoted set of numbers, between 0
+(minimum) and 100 (maximum).
+Writes to
+.B /playvol
+take the same form.
+.PP
+The
+.B /playlist
+file is an append-only file which accepts lines with one or two fields
+per line (parsed using
+.BR tokenize ).
+The first, compulsory, field is a file name, the optional second argument
+may contain a reference to, or a description of, the item, for instance in a graphical
+user interface.
+.B /playlist
+is append-only, individual lines cannot be removed.  However, the playlist
+can be cleared by opening the file with the
+.B OTRUNC
+flag.  A process that has
+.B /playlist
+open while the file is truncated will receive an error on the next read with
+.B errstr
+set to
+.IR "reading past eof" .
+When this error occurs, clients can seek to the beginning of the file and reread its contents.
+.PP
+After starting up,
+.B Playlistfs
+puts itself in the background. When called with the
+.B \-s
+flag, it posts a mountable file descriptor in
+.BR /srv/playlist.\f2postname\fP .
+The
+.B \-m
+flag can be used to specify a mount point other than
+.BR /mnt .
+.PP
+The files to be played are recognized by one of four extensions, and an appropriate
+player is then selected to play them.  Files without a recognized extension are played by the
+.I pac
+player:
+.TP
+\&.mp3
+/bin/games/mp3dec
+.TP
+\&.pac
+/bin/games/pac4dec
+.TP
+\&.pcm
+/bin/cp
+.TP
+\&.ogg
+/bin/games/vorbisdec
+.SH FILES
+.BR /srv/playlistfs.\f2user\fP :
+default
+.B playlistfs
+mountable file descriptor used by juke(7).
+.br
+.BR /mnt/playctl :
+Control file
+.br
+.BR /mnt/playlist :
+Playlist file
+.br
+.BR /mnt/playvol :
+Volume control file
+.SH SOURCE
+.B /sys/src/games/music/playlistfs
+.SH SEE ALSO
+.IR juke (7),
+.IR audio (7)

+ 8 - 0
sys/src/cmd/auth/factotum/chap.c

@@ -404,6 +404,14 @@ doLMchap(char *pass, uchar chal[ChapChallen], uchar reply[MSchapResplen])
 	ulong schedule[32];
 	uchar p14[15], p16[16];
 	uchar s8[8] = {0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25};
+	int n = strlen(pass);
+
+	if(n > 14){
+		// let prudent people avoid the LM vulnerability
+		//   and protect the loop below from buffer overflow
+		memset(reply, 0, MSchapResplen);
+		return;
+	}
 
 	// Spec says space padded, experience says otherwise
 	memset(p14, 0, sizeof p14 -1);

+ 12 - 0
sys/src/games/music/Readme

@@ -0,0 +1,12 @@
+The Plan 9 Jukebox consists of three applications: a play-list server, a browse server
+and a GUI:
+	playlist server: playlistfs
+	browse server: jukefs
+	GUI: jukebox
+The juke.rc script (installed, by default, in /rc/bin/juke) starts them all up.
+
+To configure, adjust the DEFAULTMAP and ICONPATH in the central mkfile.
+The default map is used by jukefs as the root of the descriptive database.
+The iconpath is used by jukebox to find the icons.
+
+Report problems to sape@plan9.bell-labs.com

+ 13 - 0
sys/src/games/music/debug.h

@@ -0,0 +1,13 @@
+extern int debug;
+
+enum {
+	DBGSERVER	= 0x01,
+	DBGCONTROL	= 0x02,
+	DBGCTLGRP	= 0x04,
+	DBGPICKLE	= 0x08,
+	DBGSTATE	= 0x10,
+	DBGPLAY		= 0x20,
+	DBGPUMP		= 0x40,
+	DBGTHREAD	= 0x80,
+	DBGFILEDUMP	= 0x100,
+};

BIN
sys/src/games/music/icon/next.bit


BIN
sys/src/games/music/icon/pause.bit


BIN
sys/src/games/music/icon/play.bit


BIN
sys/src/games/music/icon/prev.bit


BIN
sys/src/games/music/icon/question.bit


BIN
sys/src/games/music/icon/root.bit


BIN
sys/src/games/music/icon/skull.bit


BIN
sys/src/games/music/icon/stop.bit


BIN
sys/src/games/music/icon/trash.bit


+ 67 - 0
sys/src/games/music/juke.rc

@@ -0,0 +1,67 @@
+#!/bin/rc
+
+wide=`{echo $vgasize | sed 's/(.*)x.*x.*/\1 > 240/' | hoc}
+
+debug=0
+tflag=''
+wflag=''
+host=''
+kb=4096
+flags=()
+sname=$user
+if (! ~ $wide 1) {
+	flags=($flags -t)
+	kb=1024
+}
+while(! ~ $#* 0) {
+	switch ($1) {
+	case -d
+		debug=$2
+		shift
+	case -t
+		tflag='-t'
+	case -h
+		host=$2
+		shift
+	case -w
+		wflags='-w'
+	case -s
+		sname=$2
+		shift
+	case -*
+		echo Usage: classical [-d level] [-t] [-h srvhost]
+		exit usage
+	}
+	shift
+}
+if (! test -e /mnt/playlist){
+	if (! ~ $debug '0') echo mounting playlistfs
+	if (! test -e /srv/playlist.$sname && ! ~ $host ''){
+		import -a $host /srv /srv
+	}
+	if (! mount -b /srv/playlist.$sname /mnt >/dev/null >[2]/dev/null){
+		rm -f /srv/playlist.$sname
+		if (! test -e /sys/lib/music/classic){
+			if (! ~ $debug '0') echo connecting to choline
+			if (! test -e /n/choline/lib/audio)
+				9fs choline
+			if (! bind -a /n/choline/lib/audio /sys/lib/music)
+				exit choline
+		}
+		if (! ~ $debug '0') echo starting playlistfs
+		games/playlistfs -s $sname -d $debug
+	}
+}
+if (! test -w /mnt/juke) {
+	if (! test -e /srv/jukefs.$sname && ! ~ $host ''){
+		import -a $host /srv /srv
+	}
+	if (! mount -b /srv/jukefs.$sname /mnt >/dev/null >[2]/dev/null){
+		if (! ~ $debug '0') echo games/jukefs
+		games/jukefs -s $sname
+	}
+}
+if (~ $wflags '-w') {
+	exec games/jukebox -w -d $debug $tflag &
+}
+exec games/jukebox -d $debug $tflag

+ 149 - 0
sys/src/games/music/jukebox/client.c

@@ -0,0 +1,149 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <bio.h>
+#include "client.h"
+#include "playlist.h"
+#include "../debug.h"
+
+char *srvmount = "/mnt/juke";
+
+char*
+getroot(void)
+{
+	return "root";
+}
+
+void
+fillbrowsebot(char *onum)
+{
+	char name[256], *p, *q;
+	Biobuf *b, *d;
+	int c;
+
+	snprint(name, sizeof name, "%s/%s/children", srvmount, onum);
+	b = Bopen(name, OREAD);
+	if(b == nil)
+abort();//		sysfatal("getchildren: %s: %r", name);
+	while(p = Brdline(b, '\n')){
+		c = strtol(p, &q, 0);
+		assert(*q == '\n');
+		*q = 0;
+		snprint(name, sizeof name, "%s/%d/type", srvmount, c);
+		d = Bopen(name, OREAD);
+		if(d == nil)
+			sysfatal("getchildren: %s: %r", name);
+		q = Brdstr(d, '\n', 1);
+		if(q == nil){
+			abort();
+		}
+		Bterm(d);
+		if(strcmp(q, "performance") == 0)
+			continue;
+		snprint(name, sizeof name, "%s/%d/digest", srvmount, c);
+		d = Bopen(name, OREAD);
+		if(d == nil)
+			sysfatal("getchildren: %s: %r", name);
+		q = Brdstr(d, '\n', 1);
+		if(q == nil){
+			Bterm(d);
+			continue;
+		}
+		addchild(strdup(p), q);
+		Bterm(d);
+	}
+	Bterm(b);
+}
+
+void
+doplay(char *onum){
+	char name[256], *p, *q;
+	Biobuf *b;
+	int m;
+
+	snprint(name, sizeof name, "%s/%s/files", srvmount, onum);
+	b = Bopen(name, OREAD);
+	if(b == nil)
+abort();//		sysfatal("doplay: %s: %r", name);
+	while(p = Brdline(b, '\n')){
+		m = Blinelen(b);
+		assert(p[m-1] == '\n');
+		p[m-1] = '\0';
+		q = strchr(p, '	');
+		if(q == nil)
+			sysfatal("doplay: %s: format", name);
+		*q++ = '\0';
+		sendplaylist(strdup(q), strdup(p));
+	}
+	Bterm(b);
+}
+
+void
+fillbrowsetop(char *onum)
+{
+	char name[256], *p;
+	Biobuf *b;
+	int m;
+
+	snprint(name, sizeof name, "%s/%s/parentage", srvmount, onum);
+	b = Bopen(name, OREAD);
+	if(b == nil)
+abort();//		sysfatal("gettopwin: %s: %r", name);
+	while(p = Brdline(b, '\n')){
+		m = Blinelen(b);
+		assert(p[m-1] == '\n');
+		p[m-1] = '\0';
+		addparent(p);
+	}
+	Bterm(b);
+}
+
+void
+fillplaytext(char *onum)
+{
+	char name[256], *p;
+	Biobuf *b;
+	int m;
+
+	snprint(name, sizeof name, "%s/%s/parentage", srvmount, onum);
+	b = Bopen(name, OREAD);
+	if(b == nil)
+abort();//		sysfatal("fillplaytext: %s: %r", name);
+	while(p = Brdline(b, '\n')){
+		m = Blinelen(b);
+		assert(p[m-1] == '\n');
+		p[m-1] = '\0';
+		addplaytext(p);
+	}
+	Bterm(b);
+}
+
+char *
+getoneliner(char *onum)
+{
+	char name[256], *p;
+	Biobuf *b;
+
+	snprint(name, sizeof name, "%s/%s/miniparentage", srvmount, onum);
+	b = Bopen(name, OREAD);
+	if(b == nil)
+		sysfatal("gettopwin: %s: %r", name);
+	p = Brdstr(b, '\n', 1);
+	Bterm(b);
+	return p;
+}
+
+char *
+getparent(char *onum)
+{
+	char name[256], *p;
+	Biobuf *b;
+
+	snprint(name, sizeof name, "%s/%s/parent", srvmount, onum);
+	b = Bopen(name, OREAD);
+	if(b == nil)
+abort();//		sysfatal("gettopwin: %s: %r", name);
+	p = Brdstr(b, '\n', 1);
+	Bterm(b);
+	return p;
+}

+ 10 - 0
sys/src/games/music/jukebox/client.h

@@ -0,0 +1,10 @@
+char*	getroot(void);
+void	doplay(char*);
+void	fillbrowsebot(char*);
+void	fillbrowsetop(char*);
+void	fillplaytext(char*);
+void	addchild(char*, char*);
+void	addparent(char*);
+char	*getoneliner(char*);
+char	*getparent(char*);
+void	addplaytext(char*);

+ 120 - 0
sys/src/games/music/jukebox/colors.c

@@ -0,0 +1,120 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <draw.h>
+#include <keyboard.h>
+#include <mouse.h>
+#include <control.h>
+#include "colors.h"
+
+Font *boldfont;
+Font *romanfont;
+
+Image		*background;
+Image		*bordercolor;
+Image		*black;
+Image		*blue;
+Image		*darkblue;
+Image		*darkgrey;
+Image		*darkgreen;
+Image		*darkmagenta;
+Image		*green;
+Image		*grey;
+Image		*high;
+Image		*land;
+Image		*lightblue;
+Image		*lightgreen;
+Image		*lightgrey;
+Image		*lightmagenta;
+Image		*low;
+Image		*magenta;
+Image		*oceanblue;
+Image		*pale;
+Image		*paleblue;
+Image		*paleyellow;
+Image		*red;
+Image		*sea;
+Image		*white;
+Image		*yellow;
+
+static ulong
+rgba(ulong rgba)
+{
+	uchar r, g, b, a;
+
+	a = rgba & 0xff;
+	b = (rgba >>= 8) & 0xff;
+	g = (rgba >>= 8) & 0xff;
+	r = (rgba >> 8) & 0xff;
+	rgba = ((r * a / 0xff) & 0xff);
+	rgba = ((g * a / 0xff) & 0xff) | (rgba << 8);
+	rgba = ((b * a / 0xff) & 0xff) | (rgba << 8);
+	rgba = (a & 0xff) | (rgba << 8);
+	return rgba;
+}
+
+void
+colorinit(char *roman, char *bold)
+{
+	Rectangle r = Rect(0, 0, 1, 1);
+
+	white =			display->white;
+	black =			display->black;
+	blue =			allocimage(display, r, screen->chan, 1, rgba(0x0000ffff));
+	darkblue =		allocimage(display, r, screen->chan, 1, rgba(0x0000ccff));
+	darkgrey =		allocimage(display, r, screen->chan, 1, rgba(0x444444ff));
+	darkgreen =		allocimage(display, r, screen->chan, 1, rgba(0x008800ff));
+	darkmagenta =		allocimage(display, r, screen->chan, 1, rgba(0x770077ff));
+	green =			allocimage(display, r, screen->chan, 1, rgba(0x00ff00ff));
+	grey =			allocimage(display, r, screen->chan, 1, rgba(0x888888ff));
+	high =			allocimage(display, r, screen->chan, 1, rgba(0x00ccccff));
+	land =			allocimage(display, r, screen->chan, 1, rgba(0xe0ffe0ff));
+	lightblue =		allocimage(display, r, screen->chan, 1, rgba(0x88ccccff));
+	lightgreen =		allocimage(display, r, screen->chan, 1, rgba(0xaaffaaff));
+	lightgrey =		allocimage(display, r, screen->chan, 1, rgba(0xddddddff));
+	lightmagenta =		allocimage(display, r, screen->chan, 1, rgba(0xff88ffff));
+	low =			allocimage(display, r, screen->chan, 1, rgba(0xddddddff));
+	magenta =		allocimage(display, r, screen->chan, 1, rgba(0xbb00bbff));
+	oceanblue =		allocimage(display, r, screen->chan, 1, rgba(0x93ddddff));
+	pale =			allocimage(display, r, screen->chan, 1, rgba(0xffffaaff));
+	paleblue =		allocimage(display, r, screen->chan, 1, rgba(0xddffffff));
+	paleyellow =		allocimage(display, r, screen->chan, 1, rgba(0xeeee9eff));
+	red =			allocimage(display, r, screen->chan, 1, rgba(0xff0000ff));
+	sea =			allocimage(display, r, screen->chan, 1, rgba(0xe0e0ffff));
+	yellow =			allocimage(display, r, screen->chan, 1, rgba(0xffff00ff));
+	background = sea;
+	bordercolor = darkgreen;
+
+	namectlimage(background, "background");
+	namectlimage(bordercolor, "border");
+	namectlimage(black, "black");
+	namectlimage(blue, "blue");
+	namectlimage(darkblue, "darkblue");
+	namectlimage(darkgreen, "darkgreen");
+	namectlimage(darkmagenta, "darkmagenta");
+	namectlimage(green, "green");
+	namectlimage(grey, "grey");
+	namectlimage(high, "high");
+	namectlimage(land, "land");
+	namectlimage(lightblue, "lightblue");
+	namectlimage(lightgreen, "lightgreen");
+	namectlimage(lightgrey, "lightgrey");
+	namectlimage(lightmagenta, "lightmagenta");
+	namectlimage(low, "low");
+	namectlimage(magenta, "magenta");
+	namectlimage(oceanblue, "oceanblue");
+	namectlimage(pale, "pale");
+	namectlimage(paleblue, "paleblue");
+	namectlimage(paleyellow, "paleyellow");
+	namectlimage(red, "red");
+	namectlimage(sea, "sea");
+	namectlimage(white, "white");
+	namectlimage(yellow, "yellow");
+
+	if ((romanfont = openfont(display, roman)) == nil)
+		sysfatal("openfont %s: %r", roman);
+	namectlfont(romanfont, "romanfont");
+	if ((boldfont = openfont(display, bold)) == nil)
+		sysfatal("openfont %s: %r", bold);
+	namectlfont(boldfont, "boldfont");
+}

+ 30 - 0
sys/src/games/music/jukebox/colors.h

@@ -0,0 +1,30 @@
+extern	Image		*background;
+extern	Image		*bordercolor;
+extern	Image		*black;
+extern	Image		*blue;
+extern	Image		*darkblue;
+extern	Image		*darkgrey;
+extern	Image		*darkgreen;
+extern	Image		*darkmagenta;
+extern	Image		*green;
+extern	Image		*grey;
+extern	Image		*high;
+extern	Image		*land;
+extern	Image		*lightblue;
+extern	Image		*lightgreen;
+extern	Image		*lightgrey;
+extern	Image		*lightmagenta;
+extern	Image		*low;
+extern	Image		*magenta;
+extern	Image		*oceanblue;
+extern	Image		*pale;
+extern	Image		*paleblue;
+extern	Image		*paleyellow;
+extern	Image		*red;
+extern	Image		*sea;
+extern	Image		*white;
+extern	Image		*yellow;
+
+extern	Font			*romanfont, *boldfont;
+
+void	colorinit(char*, char*);

+ 4 - 0
sys/src/games/music/jukebox/mk.dep

@@ -0,0 +1,4 @@
+client.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/bio.h client.h playlist.h
+colors.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/draw.h /sys/include/keyboard.h /sys/include/mouse.h /sys/include/control.h colors.h
+music.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/draw.h /sys/include/keyboard.h /sys/include/mouse.h /sys/include/control.h colors.h client.h playlist.h
+playlist.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/draw.h /sys/include/keyboard.h /sys/include/mouse.h /sys/include/control.h playlist.h

+ 16 - 0
sys/src/games/music/jukebox/mkfile

@@ -0,0 +1,16 @@
+</$objtype/mkfile
+<../mkinc
+
+CFLAGS = -DDEFAULTMAP="$DEFAULTMAP" -DICONPATH="$ICONPATH"
+TARG = jukebox
+BIN = /$objtype/bin/games
+
+CFILES=\
+	client.c\
+	colors.c\
+	music.c\
+	playlist.c\
+
+OFILES = ${CFILES:%.c=%.$O}
+
+</sys/src/cmd/mkone

+ 1030 - 0
sys/src/games/music/jukebox/music.c

@@ -0,0 +1,1030 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <draw.h>
+#include <keyboard.h>
+#include <mouse.h>
+#include <control.h>
+#include "colors.h"
+#include "client.h"
+#include "playlist.h"
+#include "../debug.h"
+
+int	debug = 0; //DBGSERVER|DBGPUMP|DBGSTATE|DBGPICKLE|DBGPLAY;
+
+char	usage[] = "Usage: %s [-t]\n";
+
+typedef struct But {
+	char	*name;
+	Control	*ctl;
+} But;
+
+typedef struct Simpleitem {
+	char	*address;
+	char	*data;
+} Simpleitem;
+
+typedef struct Multiitem {
+	char	*address;
+	int	ndata;
+	char	**data;
+} Multiitem;
+
+enum {
+	WinBrowse,
+	WinPlay,
+	WinPlaylist,
+	WinError,
+	Topselect = 0x7fffffff
+};
+
+typedef enum {
+	PlayIdle,
+	PlayStart,
+	Playing,
+	PlayPause,
+} Playstate;
+
+typedef enum {
+	User,
+	Troot,
+	Rroot,
+	Tchildren,
+	Rchildren,
+	Tparent,
+	Rparent,
+	Tinfo,
+	Rinfo,
+	Tparentage,
+	Rparentage,
+	Tplay,
+	Rplay,
+} Srvstate;
+
+enum {
+	Exitbutton,
+	Pausebutton,
+	Playbutton,
+	Stopbutton,
+	Prevbutton,
+	Nextbutton,
+	Rootbutton,
+	Deletebutton,
+	Helpbutton,
+	Volume,
+	Browsetopwin,
+	Browsebotwin,
+	Playevent,
+	Playlistwin,
+	Nalt,
+};
+
+But buts[] = {
+	[Exitbutton] =		{"skull", nil},
+	[Pausebutton] =		{"pause", nil},
+	[Playbutton] =		{"play", nil},
+	[Stopbutton] =		{"stop", nil},
+	[Prevbutton] =		{"prev", nil},
+	[Nextbutton] =		{"next", nil},
+	[Rootbutton] =		{"root", nil},
+	[Deletebutton] =	{"trash", nil},
+	[Helpbutton] =		{"question", nil},
+};
+
+struct tab {
+	char *tabname;
+	char *winname;
+	Control *tab;
+	Control *win;
+} tabs[4] = {
+	[WinBrowse] =	{"Browse",	"browsewin",	nil, nil},
+	[WinPlay] =	{"Playing",	"playwin",	nil, nil},
+	[WinPlaylist] =	{"Playlist",	"listwin",	nil, nil},
+	[WinError] =	{"Errors",	"errorwin",	nil, nil},
+};
+
+char *helptext[] = {
+	"Buttons, left to right:",
+	"    Exit: exit jukebox",
+	"    Pause: pause/resume playback",
+	"    Play: play selection in Playlist",
+	"    Stop: stop playback",
+	"    Prev: play previous item in Playlist",
+	"    Next: play next item in Playlist",
+	"    Root: browse to root of database tree",
+	"    Delete: empty Playlist, reread database",
+	"    Help: show this window",
+	"",
+	"Browse window: (click tab to bring forward)",
+	"  Top window displays current item",
+	"  Bottom window displays selectable subitems",
+	"  Mouse commands:",
+	"    Left: selected subitem becomes current",
+	"    Right: parent of current item becomes current",
+	"    Middle: add item or subitem to Playlist",
+	"",
+	"Playing window",
+	"  Displays item currently playing",
+	"",
+	"Playlist window",
+	"  Displays contents of Playlist",
+	"  Mouse commands:",
+	"    Left: select item",
+	"    (then click the play button)",
+	"",
+	"Error window",
+	"  Displays error messages received from player",
+	"  (e.g., can't open file)",
+	nil,
+};
+
+Control		*vol;
+Control		*browsetopwin;
+Control		*browsebotwin;
+Control		*playlistwin;
+Control		*errortext;
+
+Playstate	playstate;
+
+ulong		playingbuts = 1<<Pausebutton | 1<<Stopbutton | 1<<Prevbutton | 1<<Nextbutton;
+ulong		activebuts;
+
+int		tabht;
+Image		*vol1img;
+Image		*vol2img;
+
+int		resizeready;
+int		borderwidth = 1;
+int		butht, butwid;
+int		errorlines;
+
+int		tflag;
+int		pflag;
+
+Controlset	*cs;
+
+char		*root;
+Multiitem	parent;
+Simpleitem	children[2048];
+int		nchildren;
+
+int		selected;
+
+Channel		*playevent;
+
+void
+readbuts(void)
+{
+	static char str[32], file[64];
+	But *b;
+	int fd;
+	Image *img, *mask;
+
+	for(b = buts; b < &buts[nelem(buts)]; b++){
+		sprint(file, "%s/%s.bit", ICONPATH, b->name);
+		if((fd = open(file, OREAD)) < 0)
+			sysfatal("open: %s: %r", file);
+		mask = readimage(display, fd, 0);
+		close(fd);
+		butwid = Dx(mask->r);
+		butht = Dy(mask->r);
+		b->ctl = createbutton(cs, b->name);
+		chanprint(cs->ctl, "%q align center", b->name);
+		chanprint(cs->ctl, "%q border 0", b->name);
+
+		img = allocimage(display, mask->r, screen->chan, 0, 0xe0e0ffff);
+		draw(img, img->r, darkgreen, mask, mask->r.min);
+		sprint(str, "%s.active", b->name);
+		namectlimage(img, str);
+
+		img = allocimage(display, mask->r, screen->chan, 0, 0xe0e0ffff);
+		draw(img, img->r, lightblue, mask, mask->r.min);
+		sprint(str, "%s.passive", b->name);
+		namectlimage(img, str);
+
+		chanprint(cs->ctl, "%q image %q", b->name, str);
+		sprint(str, "%s.mask", b->name);
+		namectlimage(mask, str);
+		chanprint(cs->ctl, "%q mask %q", b->name, str);
+		chanprint(cs->ctl, "%q light red", b->name);
+		chanprint(cs->ctl, "%q size %d %d %d %d", b->name, butwid, butht, butwid, butht);
+	}
+}
+
+void
+activatebuttons(ulong mask)
+{	// mask bit i corresponds to buts[i];
+	ulong bit;
+	But *b;
+	static char str[40];
+	int i;
+
+	for(i = 0; i < nelem(buts); i++){
+		b = &buts[i];
+		bit = 1 << i;
+		if((mask & bit) && (activebuts & bit) == 0){
+			// button was `deactive'
+			activate(b->ctl);
+			activebuts |= bit;
+			sprint(str, "%s.active", b->name);
+			chanprint(cs->ctl, "%q image %q", b->name, str);
+			chanprint(cs->ctl, "%q show", b->name);
+		}
+	}
+}
+
+void
+deactivatebuttons(ulong mask)
+{	// mask bit i corresponds with buts[i];
+	ulong bit;
+	But *b;
+	static char str[40];
+	int i;
+
+	for(i = 0; i < nelem(buts); i++){
+		b = &buts[i];
+		bit = 1 << i;
+		if((mask & bit) && (activebuts & bit)){
+			// button was active
+			deactivate(b->ctl);
+			activebuts &= ~bit;
+			sprint(str, "%s.passive", b->name);
+			chanprint(cs->ctl, "%q image %q", b->name, str);
+			chanprint(cs->ctl, "%q show", b->name);
+		}
+	}
+}
+
+void
+resizecontrolset(Controlset *){
+	static Point pol[3];
+	char *p;
+
+	if(getwindow(display, Refbackup) < 0)
+		sysfatal("getwindow");
+	draw(screen, screen->r, bordercolor, nil, screen->r.min);
+	if(!resizeready)
+		return;
+#ifndef REPLACESEMANTICS
+	if(vol1img)
+		chanprint(cs->ctl, "volume image darkgreen");
+	if(vol2img)
+		chanprint(cs->ctl, "volume indicatorcolor red");
+	chanprint(cs->ctl, "wholewin size");
+	chanprint(cs->ctl, "wholewin rect %R", screen->r);
+
+	chanprint(cs->ctl, "sync");
+	p = recvp(cs->data);
+	if(strcmp(p, "sync"))
+		sysfatal("huh?");
+	free(p);
+
+	if(vol1img){
+		freectlimage("volume.img");
+		freeimage(vol1img);
+	}
+	if(vol2img){
+		freectlimage("indicator.img");
+		freeimage(vol2img);
+	}
+	vol1img = allocimage(display, vol->rect, screen->chan, 0, 0xe0e0ffff);
+	vol2img = allocimage(display, vol->rect, screen->chan, 0, 0xe0e0ffff);
+	pol[0] = Pt(vol->rect.min.x, vol->rect.max.y);
+	pol[1] = Pt(vol->rect.max.x, vol->rect.min.y);
+	pol[2] = vol->rect.max;
+	fillpoly(vol1img, pol, 3, 0, darkgreen, ZP);
+	fillpoly(vol2img, pol, 3, 0, red, ZP);
+	namectlimage(vol1img, "volume.img");
+	namectlimage(vol2img, "indicator.img");
+	chanprint(cs->ctl, "volume image volume.img");
+	chanprint(cs->ctl, "volume indicatorcolor indicator.img");
+#else
+	chanprint(cs->ctl, "wholewin size");
+	chanprint(cs->ctl, "wholewin rect %R", screen->r);
+
+	chanprint(cs->ctl, "sync");
+	p = recvp(cs->data);
+	if(strcmp(p, "sync"))
+		sysfatal("huh?");
+	free(p);
+
+	new1img = allocimage(display, vol->rect, screen->chan, 0, 0xe0e0ffff);
+	new2img = allocimage(display, vol->rect, screen->chan, 0, 0xe0e0ffff);
+	pol[0] = Pt(vol->rect.min.x, vol->rect.max.y);
+	pol[1] = Pt(vol->rect.max.x, vol->rect.min.y);
+	pol[2] = vol->rect.max;
+	fillpoly(new1img, pol, 3, 0, darkgreen, ZP);
+	fillpoly(new2img, pol, 3, 0, red, ZP);
+	namectlimage(new1img, "volume.img");
+	namectlimage(new2img, "indicator.img");
+	if(vol1img)
+		freeimage(vol1img);
+	else
+		chanprint(cs->ctl, "volume image volume.img");
+	if(vol2img)
+		freeimage(vol2img);
+	else
+		chanprint(cs->ctl, "volume indicatorcolor indicator.img");
+	vol1img = new1img;
+	vol2img = new2img;
+#endif
+	chanprint(cs->ctl, "browsetopscr vis '%d'",
+		Dy(controlcalled("browsetopscr")->rect)/romanfont->height);
+	chanprint(cs->ctl, "browsebotscr vis '%d'",
+		Dy(controlcalled("browsebotscr")->rect)/romanfont->height);
+	chanprint(cs->ctl, "playscr vis '%d'",
+		Dy(controlcalled("playscr")->rect)/romanfont->height);
+	chanprint(cs->ctl, "playlistscr vis '%d'",
+		Dy(controlcalled("playlistscr")->rect)/romanfont->height);
+	chanprint(cs->ctl, "wholewin show");
+}
+
+void
+maketab(void)
+{
+	int i;
+
+	tabht = boldfont->height + 1 + borderwidth;
+
+	createtab(cs, "tabs");
+
+	for(i = 0; i < nelem(tabs); i++){
+		tabs[i].tab = createtextbutton(cs, tabs[i].tabname);
+		chanprint(cs->ctl, "%q size %d %d %d %d", tabs[i].tab->name,
+			stringwidth(boldfont, tabs[i].tabname), tabht, 1024, tabht);
+		chanprint(cs->ctl, "%q align uppercenter", tabs[i].tab->name);
+		chanprint(cs->ctl, "%q font boldfont", tabs[i].tab->name);
+		chanprint(cs->ctl, "%q image background", tabs[i].tab->name);
+		chanprint(cs->ctl, "%q light background", tabs[i].tab->name);
+		chanprint(cs->ctl, "%q pressedtextcolor red", tabs[i].tab->name);
+		chanprint(cs->ctl, "%q textcolor darkgreen", tabs[i].tab->name);
+		chanprint(cs->ctl, "%q mask transparent", tabs[i].tab->name);
+		chanprint(cs->ctl, "%q text %q", tabs[i].tab->name, tabs[i].tabname);
+
+		chanprint(cs->ctl, "tabs add %s %s", tabs[i].tabname, tabs[i].winname);
+	}
+
+	chanprint(cs->ctl, "tabs separation %d", 2);
+	chanprint(cs->ctl, "tabs image background");
+	chanprint(cs->ctl, "tabs value 0");
+}
+
+void
+makeplaycontrols(void)
+{
+	int w;
+	Control *playscr;
+
+	w = stringwidth(romanfont, "Roll over Beethoven");
+	playscr = createslider(cs, "playscr");
+	chanprint(cs->ctl, "playscr size 12, 24, 12, 1024");
+	createtext(cs, "playtext");
+	chanprint(cs->ctl, "playtext size %d %d %d %d",
+		w, 5*romanfont->height, 2048, 1024);
+
+	chanprint(cs->ctl, "playscr format '%%s: playtext topline %%d'");
+	controlwire(playscr, "event", cs->ctl);
+
+	tabs[WinPlay].win = createrow(cs, tabs[WinPlay].winname);
+	chanprint(cs->ctl, "%q add playscr playtext", tabs[WinPlay].win->name);
+}
+
+void
+makebrowsecontrols(void)
+{
+	int w;
+	Control *browsetopscr, *browsebotscr;
+
+	w = stringwidth(romanfont, "Roll over Beethoven");
+	browsetopscr = createslider(cs, "browsetopscr");
+	chanprint(cs->ctl, "browsetopscr size 12, 24, 12, %d", 12*romanfont->height);
+	browsetopwin = createtext(cs, "browsetopwin");
+	chanprint(cs->ctl, "browsetopwin size %d %d %d %d",
+		w, 3*romanfont->height, 2048, 12*romanfont->height);
+	createrow(cs, "browsetop");
+	chanprint(cs->ctl, "browsetop add browsetopscr browsetopwin");
+
+	browsebotscr = createslider(cs, "browsebotscr");
+	chanprint(cs->ctl, "browsebotscr size 12, 24, 12, 1024");
+	browsebotwin = createtext(cs, "browsebotwin");
+	chanprint(cs->ctl, "browsebotwin size %d %d %d %d",
+		w, 3*romanfont->height, 2048, 1024);
+	createrow(cs, "browsebot");
+	chanprint(cs->ctl, "browsebot add browsebotscr browsebotwin");
+
+	chanprint(cs->ctl, "browsetopscr format '%%s: browsetopwin topline %%d'");
+	controlwire(browsetopscr, "event", cs->ctl);
+	chanprint(cs->ctl, "browsebotscr format '%%s: browsebotwin topline %%d'");
+	controlwire(browsebotscr, "event", cs->ctl);
+
+	tabs[WinBrowse].win = createcolumn(cs, tabs[WinBrowse].winname);
+	chanprint(cs->ctl, "%q add browsetop browsebot", tabs[WinBrowse].win->name);
+}
+
+void
+makeplaylistcontrols(void)
+{
+	int w;
+	Control *playlistscr;
+
+	w = stringwidth(romanfont, "Roll over Beethoven");
+	playlistscr = createslider(cs, "playlistscr");
+	chanprint(cs->ctl, "playlistscr size 12, 24, 12, 1024");
+	playlistwin = createtext(cs, "playlistwin");
+	chanprint(cs->ctl, "playlistwin size %d %d %d %d",
+		w, 5*romanfont->height, 2048, 1024);
+//	chanprint(cs->ctl, "playlistwin selectmode multi");
+
+	chanprint(cs->ctl, "playlistscr format '%%s: playlistwin topline %%d'");
+	controlwire(playlistscr, "event", cs->ctl);
+
+	tabs[WinPlaylist].win = createrow(cs, tabs[WinPlaylist].winname);
+	chanprint(cs->ctl, "%q add playlistscr playlistwin", tabs[WinPlaylist].win->name);
+}
+
+void
+makeerrorcontrols(void)
+{
+	int w;
+	Control *errorscr;
+
+	w = stringwidth(romanfont, "Roll over Beethoven");
+	errorscr = createslider(cs, "errorscr");
+	chanprint(cs->ctl, "errorscr size 12, 24, 12, 1024");
+	errortext = createtext(cs, "errortext");
+	chanprint(cs->ctl, "errortext size %d %d %d %d",
+		w, 5*romanfont->height, 2048, 1024);
+	chanprint(cs->ctl, "errortext selectmode multi");
+
+	chanprint(cs->ctl, "errorscr format '%%s: errortext topline %%d'");
+	controlwire(errorscr, "event", cs->ctl);
+
+	tabs[WinError].win = createrow(cs, tabs[WinError].winname);
+	chanprint(cs->ctl, "%q add errorscr errortext", tabs[WinError].win->name);
+}
+
+void
+makecontrols(void)
+{
+	int i;
+
+	cs = newcontrolset(screen, nil, nil, nil);
+
+	// make shared buttons
+	readbuts();
+
+	vol = createslider(cs, "volume");
+	chanprint(cs->ctl, "volume size %d %d %d %d", 2*butwid, butht, 2048, butht);
+	chanprint(cs->ctl, "volume absolute 1");
+	chanprint(cs->ctl, "volume indicatorcolor red");
+	chanprint(cs->ctl, "volume max 100");
+	chanprint(cs->ctl, "volume orient hor");
+	chanprint(cs->ctl, "volume clamp low 1");
+	chanprint(cs->ctl, "volume clamp high 0");
+	chanprint(cs->ctl, "volume format '%%s volume %%d'");
+
+	createrow(cs, "buttonrow");
+	for(i = 0; i < nelem(buts); i++)
+		chanprint(cs->ctl, "buttonrow add %s", buts[i].name);
+	chanprint(cs->ctl, "buttonrow add volume");
+	chanprint(cs->ctl, "buttonrow separation %d", borderwidth);
+	chanprint(cs->ctl, "buttonrow image darkgreen");
+
+	makebrowsecontrols();
+	makeplaycontrols();
+	makeplaylistcontrols();
+	makeerrorcontrols();
+
+	maketab();
+
+	chanprint(cs->ctl, "%q image background", "text slider");
+	chanprint(cs->ctl, "text font romanfont");
+	chanprint(cs->ctl, "slider indicatorcolor darkgreen");
+	chanprint(cs->ctl, "row separation %d", borderwidth);
+	chanprint(cs->ctl, "row image darkgreen");
+	chanprint(cs->ctl, "column separation %d", 2);
+	chanprint(cs->ctl, "column image darkgreen");
+
+	createcolumn(cs, "wholewin");
+	chanprint(cs->ctl, "wholewin separation %d", borderwidth);
+	chanprint(cs->ctl, "wholewin add buttonrow tabs");
+	chanprint(cs->ctl, "wholewin image darkgreen");
+	chanprint(cs->ctl, "%q image darkgreen", "column row");
+}
+
+void
+makewindow(int dx, int dy, int wflag){
+	int mountfd, fd, n;
+	static char aname[128];
+	static char rio[128] = "/mnt/term";
+	char *args[6];
+
+	if(wflag){
+		/* find out screen size */
+		fd = open("/mnt/wsys/screen", OREAD);
+		if(fd >= 0 && read(fd, aname, 60) == 60){
+			aname[60] = '\0';
+			n = tokenize(aname, args, nelem(args));
+			if(n != 5)
+				fprint(2, "Not an image: /mnt/wsys/screen\n");
+			else{
+				n = atoi(args[3]) - atoi(args[1]);
+				if(n <= 0 || n > 2048)
+					fprint(2, "/mnt/wsys/screen very wide: %d\n", n);
+				else
+					if(n < dx) dx = n-1;
+				n = atoi(args[4]) - atoi(args[2]);
+				if(n <= 0 || n > 2048)
+					fprint(2, "/mnt/wsys/screen very high: %d\n", n);
+				else
+					if(n < dy) dy = n-1;
+			}
+			close(fd);
+		}
+		n = 0;
+		if((fd = open("/env/wsys", OREAD)) < 0){
+			n = strlen(rio);
+			fd = open("/mnt/term/env/wsys", OREAD);
+			if(fd < 0)
+				sysfatal("/env/wsys");
+		}
+		if(read(fd, rio+n, sizeof(rio)-n-1) <= 0)
+			sysfatal("/env/wsys");
+		mountfd = open(rio, ORDWR);
+		if(mountfd < 0)
+			sysfatal("open %s: %r", rio);
+		snprint(aname, sizeof aname, "new -dx %d -dy %d", dx, dy);
+		rfork(RFNAMEG);
+		if(mount(mountfd, -1, "/mnt/wsys", MREPL, aname) < 0)
+			sysfatal("mount: %r");
+		if(bind("/mnt/wsys", "/dev", MBEFORE) < 0)
+			sysfatal("mount: %r");
+	}
+
+	if(initdraw(nil, nil, "music") < 0)
+		sysfatal("initdraw: %r");
+
+	initcontrols();
+	if(dx <= 320)
+		colorinit("/lib/font/bit/lucidasans/unicode.6.font",
+			"/lib/font/bit/lucidasans/boldunicode.8.font");
+	else
+		colorinit("/lib/font/bit/lucidasans/unicode.8.font",
+			"/lib/font/bit/lucidasans/boldunicode.10.font");
+	makecontrols();
+	resizeready = 1;
+
+	resizecontrolset(cs);
+	if(debug & DBGCONTROL)
+		fprint(2, "resize done\n");
+}
+
+void
+setparent(char *addr)
+{
+	int i;
+
+	if(parent.address)
+		free(parent.address);
+	parent.address = strdup(addr);
+	for(i = 0; i < parent.ndata; i++)
+		if(parent.data[i])
+			free(parent.data[i]);
+	parent.ndata = 0;
+	if(parent.data){
+		free(parent.data);
+		parent.data = nil;
+	}
+	chanprint(cs->ctl, "browsetopwin clear");
+	chanprint(cs->ctl, "browsetopscr max 0");
+	chanprint(cs->ctl, "browsetopscr value 0");
+}
+
+void
+addparent(char *str)
+{
+	parent.data = realloc(parent.data, (parent.ndata+1)*sizeof(char*));
+	parent.data[parent.ndata] = strdup(str);
+	parent.ndata++;
+	chanprint(cs->ctl, "browsetopwin accumulate %q", str);
+	chanprint(cs->ctl, "browsetopscr max %d", parent.ndata);
+}
+
+void
+clearchildren(void)
+{
+	int i;
+
+	for(i = 0; i < nchildren; i++){
+		if(children[i].address)
+			free(children[i].address);
+		if(children[i].data)
+			free(children[i].data);
+	}
+	nchildren= 0;
+	chanprint(cs->ctl, "browsebotwin clear");
+	chanprint(cs->ctl, "browsebotwin topline 0");
+	chanprint(cs->ctl, "browsebotscr max 0");
+	chanprint(cs->ctl, "browsebotscr value 0");
+	selected = -1;
+}
+
+void
+addchild(char *addr, char *data)
+{
+	children[nchildren].address = addr;
+	children[nchildren].data = data;
+	nchildren++;
+	chanprint(cs->ctl, "browsebotwin accumulate %q", data);
+	chanprint(cs->ctl, "browsebotscr max %d", nchildren);
+}
+
+static void
+playlistselect(int n)
+{
+	if(playlist.selected >= 0 && playlist.selected < playlist.nentries){
+		chanprint(cs->ctl, "playlistwin select %d 0", playlist.selected);
+		deactivatebuttons(1<<Playbutton);
+	}
+	playlist.selected = n;
+	if(playlist.selected < 0 || playlist.selected >= playlist.nentries)
+		return;
+	activatebuttons(1<<Playbutton);
+	chanprint(cs->ctl, "playlistwin select %d 1", n);
+	if(n >= 0 && n <= playlist.nentries - Dy(playlistwin->rect)/romanfont->height)
+		n--;
+	else
+		n = playlist.nentries - Dy(playlistwin->rect)/romanfont->height + 1;
+	if(n < 0) n = 0;
+	if(n < playlist.nentries){
+		chanprint(cs->ctl, "playlistwin topline %d",  n);
+		chanprint(cs->ctl, "playlistscr value %d",  n);
+	}
+	chanprint(cs->ctl, "playlist show");
+}
+
+void
+updateplaylist(int trunc)
+{
+	char *s;
+	int fd;
+
+	while(cs->ctl->s - cs->ctl->n < cs->ctl->s/4)
+		sleep(100);
+	if(trunc){
+		playlistselect(-1);
+		chanprint(cs->ctl, "playlistwin clear");
+		chanprint(cs->ctl, "playlistscr max 0");
+		chanprint(cs->ctl, "playlistscr value 0");
+		deactivatebuttons(1<<Playbutton | 1<<Deletebutton);
+		chanprint(cs->ctl, "playlistwin show");
+		chanprint(cs->ctl, "playlistscr show");
+		s = smprint("%s/ctl", srvmount);
+		if((fd = open(s, OWRITE)) >= 0){
+			fprint(fd, "reread");
+			close(fd);
+		}
+		free(s);
+		return;
+	}
+	if(playlist.entry[playlist.nentries].onum){
+		s = getoneliner(playlist.entry[playlist.nentries].onum);
+		chanprint(cs->ctl, "playlistwin accumulate %q", s);
+		free(s);
+	}else
+		chanprint(cs->ctl, "playlistwin accumulate %q", playlist.entry[playlist.nentries].file);
+	playlist.nentries++;
+	chanprint(cs->ctl, "playlistscr max %d", playlist.nentries);
+	if(playlist.selected == playlist.nentries - 1)
+		playlistselect(playlist.selected);
+	activatebuttons(1<<Playbutton|1<<Deletebutton);
+	chanprint(cs->ctl, "playlistscr show");
+	chanprint(cs->ctl, "playlistwin show");
+}
+
+void
+browseto(char *onum)
+{
+	onum = strdup(onum);
+	setparent(onum);
+	clearchildren();
+	fillbrowsetop(onum);
+	chanprint(cs->ctl, "browsetop show");
+	fillbrowsebot(onum);
+	chanprint(cs->ctl, "browsebot show");
+	free(onum);
+}
+
+void
+addplaytext(char *s)
+{
+	chanprint(cs->ctl, "playtext accumulate %q", s);
+}
+
+void
+work(void)
+{
+	static char *eventstr, *args[64], *s;
+	static char buf[4096];
+	int a, n, i;
+	Alt alts[] = {
+	[Exitbutton] =		{buts[Exitbutton].ctl->event, &eventstr, CHANRCV},
+	[Pausebutton] =		{buts[Pausebutton].ctl->event, &eventstr, CHANRCV},
+	[Playbutton] =		{buts[Playbutton].ctl->event, &eventstr, CHANRCV},
+	[Stopbutton] =		{buts[Stopbutton].ctl->event, &eventstr, CHANRCV},
+	[Prevbutton] =		{buts[Prevbutton].ctl->event, &eventstr, CHANRCV},
+	[Nextbutton] =		{buts[Nextbutton].ctl->event, &eventstr, CHANRCV},
+	[Rootbutton] =		{buts[Rootbutton].ctl->event, &eventstr, CHANRCV},
+	[Deletebutton] =	{buts[Deletebutton].ctl->event, &eventstr, CHANRCV},
+	[Helpbutton] =		{buts[Helpbutton].ctl->event, &eventstr, CHANRCV},
+	[Volume] =		{vol->event, &eventstr, CHANRCV},
+	[Browsetopwin] =	{browsetopwin->event, &eventstr, CHANRCV},
+	[Browsebotwin] =	{browsebotwin->event, &eventstr, CHANRCV},
+	[Playevent] =		{playevent, &eventstr, CHANRCV},
+	[Playlistwin] =		{playlistwin->event, &eventstr, CHANRCV},
+	[Nalt] =		{nil, nil, CHANEND}
+	};
+
+	activate(vol);
+	activate(controlcalled("tabs"));
+	activatebuttons(1 << Exitbutton | 1 << Rootbutton | 1 << Helpbutton);
+	
+	root = getroot();
+	setparent(root);
+	clearchildren();
+	addparent("Root");
+	chanprint(cs->ctl, "browsetop show");
+	fillbrowsebot(root);
+	chanprint(cs->ctl, "browsebot show");
+
+	eventstr = nil;
+	selected = -1;
+
+	for(;;){
+		a = alt(alts);
+		if(debug & DBGCONTROL)
+			fprint(2, "Event: %s\n", eventstr);
+		n = tokenize(eventstr, args, nelem(args));
+		switch(a){
+		default:
+			sysfatal("Illegal event %d in work", a);
+		case Volume:
+			if(n != 3 || strcmp(args[0], "volume") || strcmp(args[1], "volume"))
+				sysfatal("Bad Volume event[%d]: %s %s", n, args[0], args[1]);
+			setvolume(args[2]);
+			break;
+		case Exitbutton:
+			return;
+		case Pausebutton:
+			if(n != 3 || strcmp(args[0], "pause:") || strcmp(args[1], "value"))
+				sysfatal("Bad Pausebutton event[%d]: %s %s", n, args[0], args[1]);
+			if(strcmp(args[2], "0") == 0)
+				fprint(playctlfd, "resume");
+			else
+				fprint(playctlfd, "pause");
+			break;
+		case Playbutton:
+			if(n != 3 || strcmp(args[0], "play:") || strcmp(args[1], "value"))
+				sysfatal("Bad Playbutton event[%d]: %s %s", n, args[0], args[1]);
+			if(playlist.selected >= 0){
+				fprint(playctlfd, "play %d", playlist.selected);
+			}else
+				fprint(playctlfd, "play");
+			break;
+		case Stopbutton:
+			if(n != 3 || strcmp(args[0], "stop:") || strcmp(args[1], "value"))
+				sysfatal("Bad Stopbutton event[%d]: %s %s", n, args[0], args[1]);
+			if(strcmp(args[2], "0") == 0)
+				chanprint(cs->ctl, "%q value 1", buts[Stopbutton].ctl->name);
+			fprint(playctlfd, "stop");
+			break;
+		case Prevbutton:
+			if(n != 3 || strcmp(args[0], "prev:") || strcmp(args[1], "value"))
+				sysfatal("Bad Prevbutton event[%d]: %s %s", n, args[0], args[1]);
+			if(strcmp(args[2], "0") == 0)
+				break;
+			chanprint(cs->ctl, "%q value 0", buts[Prevbutton].ctl->name);
+			fprint(playctlfd, "skip -1");
+			break;
+		case Nextbutton:
+			if(n != 3 || strcmp(args[0], "next:") || strcmp(args[1], "value"))
+				sysfatal("Bad Nextbutton event[%d]: %s %s", n, args[0], args[1]);
+			if(strcmp(args[2], "0") == 0)
+				break;
+			chanprint(cs->ctl, "%q value 0", buts[Nextbutton].ctl->name);
+			fprint(playctlfd, "skip 1");
+			break;
+		case Playlistwin:
+			if(debug & (DBGCONTROL|DBGPLAY))
+				fprint(2, "Playlistevent: %s %s\n", args[0], args[1]);
+			if(n != 4 || strcmp(args[0], "playlistwin:") || strcmp(args[1], "select"))
+				sysfatal("Bad Playlistwin event[%d]: %s %s", n, args[0], args[1]);
+			n = atoi(args[2]);
+			if(n < 0 || n >= playlist.nentries)
+				sysfatal("Selecting line %d of %d", n, playlist.nentries);
+			if(playlist.selected >= 0 && playlist.selected < playlist.nentries){
+				chanprint(cs->ctl, "playlistwin select %d 0", playlist.selected);
+				chanprint(cs->ctl, "playlistwin show");
+			}
+			playlist.selected = -1;
+			deactivatebuttons(1<<Playbutton);
+			if(strcmp(args[3], "1") == 0)
+				playlistselect(n);
+			break;
+		case Rootbutton:
+			chanprint(cs->ctl, "%q value 0", buts[Rootbutton].ctl->name);
+			setparent(root);
+			clearchildren();
+			addparent("Root");
+			chanprint(cs->ctl, "browsetop show");
+			fillbrowsebot(root);
+			chanprint(cs->ctl, "browsebot show");
+			break;
+		case Deletebutton:
+			if(n != 3 || strcmp(args[0], "trash:") || strcmp(args[1], "value"))
+				sysfatal("Bad Deletebutton event[%d]: %s %s", n, args[0], args[1]);
+			if(strcmp(args[2], "0") == 0)
+				break;
+			chanprint(cs->ctl, "%q value 0", buts[Deletebutton].ctl->name);
+			sendplaylist(nil, nil);
+			break;
+		case Helpbutton:
+			chanprint(cs->ctl, "%q value 0", buts[Helpbutton].ctl->name);
+			if(errorlines > 0){
+				chanprint(cs->ctl, "errortext clear");
+				chanprint(cs->ctl, "errortext topline 0");
+				chanprint(cs->ctl, "errorscr max 0");
+				chanprint(cs->ctl, "errorscr value 0");
+			}
+			if(errorlines >= 0){
+				for(i = 0; helptext[i]; i++)
+					chanprint(cs->ctl, "errortext accumulate %q", helptext[i]);
+				chanprint(cs->ctl, "errorscr max %d", i);
+			}
+			chanprint(cs->ctl, "errortext topline 0");
+			chanprint(cs->ctl, "errorscr value 0");
+			errorlines = -1;
+			chanprint(cs->ctl, "tabs value %d", WinError);
+			break;
+		case Browsetopwin:
+			if(n != 4 || strcmp(args[0], "browsetopwin:") || strcmp(args[1], "select"))
+				sysfatal("Bad Browsetopwin event[%d]: %s %s", n, args[0], args[1]);
+			if(strcmp(args[3], "0") == 0)
+				break;
+			chanprint(cs->ctl, "browsetopwin select %s 0", args[2]);
+			selected = -1;
+			if(strcmp(args[3], "2") == 0)
+				doplay(parent.address);
+			else if(strcmp(args[3], "4") == 0){
+				s = getparent(parent.address);
+				browseto(s);
+			}
+			break;
+		case Browsebotwin:
+			if(n != 4 || strcmp(args[0], "browsebotwin:") || strcmp(args[1], "select"))
+				sysfatal("Bad Browsebotwin event[%d]: %s %s", n, args[0], args[1]);
+			n = atoi(args[2]);
+			if(n < 0 || n >= nchildren)
+				sysfatal("Selection out of range: %d [%d]", n, nchildren);
+			if(strcmp(args[3], "0") == 0){
+				selected = -1;
+				break;
+			}
+			if(n < 0)
+				break;
+			chanprint(cs->ctl, "browsebotwin select %d 0", n);
+			selected = n;
+			if(selected >= nchildren)
+				sysfatal("Select out of range: %d [0⋯%d)", selected, nchildren);
+			if(strcmp(args[3], "1") == 0){
+				browseto(children[selected].address);
+			}else if(strcmp(args[3], "2") == 0)
+				doplay(children[selected].address);
+			else if(strcmp(args[3], "4") == 0)
+				browseto(getparent(parent.address));
+			break;
+		case Playevent:
+			if(n < 3 || strcmp(args[0], "playctlproc:"))
+				sysfatal("Bad Playevent event[%d]: %s", n, args[0]);
+			if(debug & (DBGCONTROL|DBGPLAY))
+				fprint(2, "Playevent: %s %s\n", args[1], args[2]);
+			if(strcmp(args[1], "error") ==0){
+				if(n != 4){
+					fprint(2, "Playevent: %s: arg count: %d\n", args[1], n);
+					break;
+				}
+				if(errorlines < 0){
+					chanprint(cs->ctl, "errortext clear");
+					chanprint(cs->ctl, "errortext topline 0");
+					chanprint(cs->ctl, "errorscr max 0");
+					chanprint(cs->ctl, "errorscr value 0");
+					errorlines = 0;
+				}
+				n = errorlines;
+				chanprint(cs->ctl, "errortext accumulate %q", args[3]);
+				chanprint(cs->ctl, "errorscr max %d", ++errorlines);
+				if(n >= 0 && n <= errorlines - Dy(errortext->rect)/romanfont->height)
+					n--;
+				else
+					n = errorlines - Dy(errortext->rect)/romanfont->height + 1;
+				if(n < 0) n = 0;
+				if(n < errorlines){
+					chanprint(cs->ctl, "errortext topline %d",  n);
+					chanprint(cs->ctl, "errorscr value %d",  n);
+				}
+				chanprint(cs->ctl, "tabs value %d", WinError);
+			}else if(strcmp(args[1], "play") ==0){
+				chanprint(cs->ctl, "%q value 1", buts[Playbutton].ctl->name);
+				chanprint(cs->ctl, "%q value 0", buts[Stopbutton].ctl->name);
+				chanprint(cs->ctl, "%q value 0", buts[Pausebutton].ctl->name);
+				playlistselect(strtoul(args[2], nil, 0));
+				chanprint(cs->ctl, "playtext clear");
+				chanprint(cs->ctl, "playtext topline 0");
+				chanprint(cs->ctl, "playscr max 0");
+				chanprint(cs->ctl, "playscr value 0");
+				playstate = Playing;
+				activatebuttons(playingbuts);
+				qlock(&playlist);
+				if(playlist.selected < playlist.nentries){
+					fillplaytext(playlist.entry[playlist.selected].onum);
+					chanprint(cs->ctl, "playscr max %d", n);
+				}
+				qunlock(&playlist);
+				chanprint(cs->ctl, "playwin show");
+			}else if(strcmp(args[1], "stop") ==0){
+				chanprint(cs->ctl, "%q value 0", buts[Playbutton].ctl->name);
+				chanprint(cs->ctl, "%q value 1", buts[Stopbutton].ctl->name);
+				chanprint(cs->ctl, "%q value 0", buts[Pausebutton].ctl->name);
+				playlistselect(strtoul(args[2], nil, 0));
+				chanprint(cs->ctl, "%q show", tabs[WinPlaylist].winname);
+				playstate = PlayIdle;
+				deactivatebuttons(playingbuts);
+			}else if(strcmp(args[1], "pause") ==0){
+				activatebuttons(playingbuts);
+				chanprint(cs->ctl, "%q value 1", buts[Playbutton].ctl->name);
+				chanprint(cs->ctl, "%q value 0", buts[Stopbutton].ctl->name);
+				if(playstate == PlayPause){
+					chanprint(cs->ctl, "%q value 0", buts[Pausebutton].ctl->name);
+					playstate = Playing;
+				}else{
+					chanprint(cs->ctl, "%q value 1", buts[Pausebutton].ctl->name);
+					playstate = PlayPause;
+				}
+			}else if(strcmp(args[1], "exits") ==0){
+				threadexits("exitevent");
+			}else{
+				fprint(2, "Unknown play event:");
+				for(i=0; i<n; i++)
+					fprint(2, " %s", args[i]);
+				fprint(2, "\n");
+			}
+			break;
+		}
+		if(eventstr){
+			free(eventstr);
+			eventstr = nil;
+		}
+	}
+}
+
+void
+threadmain(int argc, char *argv[]){
+	int wflag;
+
+	wflag = 0;
+	ARGBEGIN{
+	case 'd':
+		debug = strtol(ARGF(), nil, 0);
+		break;
+	case 't':
+		tflag = 1;
+		break;
+	case 'w':
+		wflag = 1;
+		break;
+	default:
+		sysfatal(usage, argv0);
+	}ARGEND
+
+	quotefmtinstall();
+
+	if(tflag)
+		makewindow(320, 320, wflag);
+	else
+		makewindow(480, 480, wflag);
+
+	playlist.selected = -1;
+
+	playctlfd = open(playctlfile, OWRITE);
+	if(playctlfd < 0)
+		sysfatal("%s: %r", playctlfile);
+	proccreate(playlistproc, nil, 8192);
+	playevent = chancreate(sizeof(char *), 1);
+	proccreate(playctlproc, playevent, 8192);
+	proccreate(playvolproc, cs->ctl, 8192);
+
+	work();
+
+	closecontrolset(cs);
+	sysfatal("");
+}

+ 289 - 0
sys/src/games/music/jukebox/playlist.c

@@ -0,0 +1,289 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <draw.h>
+#include <keyboard.h>
+#include <mouse.h>
+#include <control.h>
+#include "playlist.h"
+#include "../debug.h"
+
+char *playlistfile = "/mnt/playlist";
+char *playctlfile = "/mnt/playctl";
+char *playvolfile = "/mnt/playvol";
+char *volumefile = "/dev/audioctl";
+
+Playlist	playlist;
+int		playctlfd;
+
+void
+playlistproc(void*)
+{
+	int fd, m, n, nf;
+	static char buf[8192+1];
+	char *p, *q, *fields[4];
+
+	threadsetname("playlistproc");
+	fd = open(playlistfile, OREAD);
+	if(fd < 0)
+		sysfatal("%s: %r", playlistfile);
+	p = buf;
+	n = 0;
+	if(debug & DBGPLAY)
+		fprint(2, "playlistproc: starting\n");
+	for(;;){
+		m = read(fd, buf+n, sizeof buf - 1 - n);
+		if(m == 0){
+			if(debug & DBGPLAY)
+				fprint(2, "playlistproc: empty read\n");
+			continue;
+		}
+		if(m < 0){
+			rerrstr(buf, sizeof(buf));
+			if(strcmp(buf, "reading past eof"))
+				sysfatal("%s: %r", playlistfile);
+			for(n = 0; n < playlist.nentries; n++){
+				free(playlist.entry[n].file);
+				free(playlist.entry[n].onum);
+			}
+			if(debug & DBGPLAY)
+				fprint(2, "playlistproc: trunc\n");
+			playlist.nentries = 0;
+			free(playlist.entry);
+			playlist.entry = nil;
+			updateplaylist(1);
+			seek(fd, 0, 0);
+			p = buf;
+			n = 0;
+			continue;
+		}
+		if(debug & DBGPLAY)
+			fprint(2, "playlistproc: read %d bytes\n", m);
+		n += m;
+		p[n] = '\0';
+		while(q = strchr(p, '\n')){
+			*q = 0;
+			nf = tokenize(p, fields, nelem(fields));
+			if(nf){
+				playlist.entry = realloc(playlist.entry, (playlist.nentries+1)*sizeof playlist.entry[0]);
+				if(playlist.entry == nil)
+					sysfatal("realloc %r");
+				playlist.entry[playlist.nentries].file = strdup(fields[0]);
+				if(nf > 1){
+					playlist.entry[playlist.nentries].onum = strdup(fields[1]);
+					if(debug & DBGPLAY)
+						fprint(2, "playlistproc: [%d]: %q %q\n", playlist.nentries,
+							playlist.entry[playlist.nentries].file,
+							playlist.entry[playlist.nentries].onum);
+				}else{
+					playlist.entry[playlist.nentries].onum = nil;
+					if(debug & DBGPLAY)
+						fprint(2, "playlistproc: [%d]: %q nil\n", playlist.nentries,
+							playlist.entry[playlist.nentries].file);
+				}
+				updateplaylist(0);	// this will also update nentries
+			}
+			q++;
+			n -= q-p;
+			p = q;
+		}
+		if(n)
+			memmove(buf, p, n);
+		p = buf;
+	}
+}
+
+void
+sendplaylist(char *file, char *onum)
+{
+	static int fd = -1;
+	char *b;
+
+	if(file == nil){
+		if(fd >= 0)
+			close(fd);
+		fd = open(playlistfile, OWRITE|OTRUNC);
+		if(fd < 0)
+			sysfatal("%s: truncate: %r", playlistfile);
+		return;
+	}
+	if(fd < 0){
+		fd = open(playlistfile, OWRITE);
+		if(fd < 0)
+			sysfatal("%s: %r", playlistfile);
+	}
+	b = smprint("%q	%q\n", file, onum);
+	if(debug & DBGPLAY)
+		fprint(2, "sendplaylist @%s@\n", b);
+	if(write(fd , b, strlen(b)) != strlen(b))
+		sysfatal("sendplaylist: %r");
+}
+
+void
+playctlproc(void*a)
+{
+	int fd, n, nf;
+	static char buf[512+1];
+	char *fields[4];
+	Channel *chan;
+
+	threadsetname("playctlproc");
+	chan = a;
+	fd = open(playctlfile, OREAD);
+	if(fd < 0)
+		sysfatal("%s: %r", playctlfile);
+	for(;;){
+		n = read(fd, buf, sizeof buf -1);
+		if(n == 0)
+			continue;
+		if(n < 0)
+			sysfatal("%s: %r", playctlfile);
+		buf[n] = '\0';
+		nf = tokenize(buf, fields, nelem(fields));
+		if(nf == 0)
+			continue;
+		switch (nf){
+		default:
+			sysfatal("playctlproc: [%d]: %s", nf, fields[0]);
+		case 3:
+			chanprint(chan, "playctlproc: error %lud %q", strtoul(fields[1], nil, 0), fields[2]);
+			if(strcmp(fields[0], "error") == 0)
+				break;
+			// fall through
+		case 2:
+			chanprint(chan, "playctlproc: %s %lud", fields[0], strtoul(fields[1], nil, 0));
+		}
+	}
+}
+
+void
+sendplayctl(char *fmt, ...)
+{
+	va_list arg;
+	static int fd = -1;
+
+	va_start(arg, fmt);
+	if(debug & DBGPLAY){
+		fprint(2, "sendplayctl: fmt=%s: ", fmt);
+		fprint(2, fmt, arg);
+		fprint(2, "\n");
+	}
+	fprint(fd, fmt, arg);
+	va_end(arg);
+}
+
+void
+setvolume(char *volume)
+{
+	static int fd;
+
+	if(fd == 0){
+		fd = open(playvolfile, OWRITE);
+		if(fd < 0){
+			fprint(2, "can't open %s, (%r) opening %s instead\n", playvolfile, "/dev/volume");
+			if((fd = open("/dev/volume", OWRITE)) < 0){
+				fprint(2, "setvolume: open: %r\n");
+				return;
+			}
+		}
+	}
+	if(fd < 0)
+		return;
+	fprint(fd, "volume %s", volume);
+}
+
+void
+volumeproc(void *arg)
+{
+	int fd, n, nf, nnf, i, nlines;
+	static char buf[1024];
+	char *lines[32];
+	char *fields[8];
+	char *subfields[8];
+	Channel *ctl;
+	int volume, minvolume, maxvolume, nvolume;
+
+	ctl = arg;
+	threadsetname("volumeproc");
+	fd = open(volumefile, OREAD);
+	if(fd < 0){
+		fprint(2, "%s: %r\n", volumefile);
+		threadexits(nil);
+	}
+	for(;;){
+		n = read(fd, buf, sizeof buf -1);
+		if(n == 0)
+			continue;
+		if(n < 0){
+			fprint(2, "volumeproc: read: %r\n");
+			threadexits("volumeproc");
+		}
+		buf[n] = '\0';
+		nlines = getfields(buf, lines, nelem(lines), 1, "\n");
+		for(i = 0; i < nlines; i++){
+			nf = tokenize(lines[i], fields, nelem(fields));
+			if(nf == 0)
+				continue;
+			if(nf != 6 || strcmp(fields[0], "volume") || strcmp(fields[1], "out"))
+				continue;
+			minvolume = strtol(fields[3], nil, 0);
+			maxvolume = strtol(fields[4], nil, 0);
+			if(minvolume >= maxvolume)
+				continue;
+			nnf = tokenize(fields[2], subfields, nelem(subfields));
+			if(nnf <= 0 || nnf > 8){
+				fprint(2, "volume format error\n");
+				threadexits(nil);
+			}
+			volume = 0;
+			nvolume = 0;
+			for(i = 0; i < nnf; i++){
+				volume += strtol(subfields[i], nil, 0);
+				nvolume++;
+			}
+			volume /= nvolume;
+			volume = 100*(volume - minvolume)/(maxvolume-minvolume);
+			chanprint(ctl, "volume value %d", volume);
+		}
+	}
+}
+
+void
+playvolproc(void*a)
+{
+	int fd, n, nf, volume, nvolume, i;
+	static char buf[256+1];
+	char *fields[3], *subfields[9];
+	Channel *chan;
+
+	threadsetname("playvolproc");
+	chan = a;
+	fd = open(playvolfile, OREAD);
+	if(fd < 0)
+		sysfatal("%s: %r", playvolfile);
+	for(;;){
+		n = read(fd, buf, sizeof buf -1);
+		if(n == 0)
+			continue;
+		if(n < 0)
+			sysfatal("%s: %r", playvolfile);
+		buf[n] = '\0';
+		if(debug) fprint(2, "volumestring: %s\n", buf);
+		nf = tokenize(buf, fields, nelem(fields));
+		if(nf == 0)
+			continue;
+		if(nf != 2 || strcmp(fields[0], "volume"))
+			sysfatal("playvolproc: [%d]: %s", nf, fields[0]);
+
+		nvolume = tokenize(fields[1], subfields, nelem(subfields));
+		if(nvolume <= 0 || nvolume > 8){
+			fprint(2, "volume format error\n");
+			threadexits(nil);
+		}
+		volume = 0;
+		for(i = 0; i < nvolume; i++)
+			volume += strtol(subfields[i], nil, 0);
+		volume /= nvolume;
+		chanprint(chan, "volume value %d", volume);
+	}
+}

+ 26 - 0
sys/src/games/music/jukebox/playlist.h

@@ -0,0 +1,26 @@
+
+typedef struct Playlistentry {
+	char	*file;
+	char	*onum;
+} Playlistentry;
+
+typedef struct Playlist {
+	QLock;
+	int		nentries;
+	int		selected;
+	Playlistentry	*entry;
+} Playlist;
+
+extern Playlist	playlist;
+extern char	*playctlfile;
+extern char	*srvmount;
+extern int	playctlfd;
+
+void	playctlproc(void*a);
+void	playlistproc(void*);
+void	playvolproc(void*a);
+void	sendplayctl(char *fmt, ...);
+void	sendplaylist(char*, char*);
+void	setvolume(char *volume);
+void	updateplaylist(int);
+void	volumeproc(void *arg);

+ 152 - 0
sys/src/games/music/jukefs/catset.c

@@ -0,0 +1,152 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+#include "object.h"
+#include "catset.h"
+
+static int debug = 0;
+
+int
+catsetneeded(int v)
+{
+	return (v / 8) + 1;
+}
+
+static void
+catsetprint(int f, Catset*cs)
+{
+	int i;
+	fprint(2, "(%p %d:", cs->bitpiece, cs->nbitpiece);
+	for (i = 0; i < cs->nbitpiece; i++)
+		fprint(f, "[%d]=%x", i, cs->bitpiece[i]);
+	fprint(2, ")");
+}
+
+void
+catsetrealloc(Catset *cs, int sz)
+{
+	if (debug) fprint(2, "catsetrealloc %p %d (%p %d)", cs, sz, cs->bitpiece, cs->nbitpiece);
+	if (sz > cs->nbitpiece) {
+		cs->bitpiece = realloc(cs->bitpiece, sz*sizeof(uchar));
+		memset(cs->bitpiece + cs->nbitpiece, 0, sz - cs->nbitpiece);
+		cs->nbitpiece = sz;
+	}
+	if (debug) fprint(2, " -> %p %d\n", cs->bitpiece, cs->nbitpiece);
+}
+
+void
+catsetfree(Catset *cs)
+{
+	free(cs->bitpiece);
+	cs->bitpiece = 0;
+	cs->nbitpiece = 0;
+}
+
+void
+catsetinit(Catset*cs, int v)
+{
+	int n;
+
+	n = catsetneeded(v);
+	if (debug) fprint(2, "catsetinit %p %d -> ", cs, v);
+	catsetrealloc(cs, n);
+	catsetset(cs, v);
+	if (debug) catsetprint(2, cs);
+	if (debug) fprint(2, "\n");
+}
+
+void
+catsetcopy(Catset*dst, Catset*src)
+{
+	if (debug) fprint(2, "catsetcopy %p %p ", dst, src);
+	if (debug) catsetprint(2, dst);
+	if (debug) fprint(2, " ");
+	if (debug) catsetprint(2, src);
+	if (dst->nbitpiece < src->nbitpiece)
+		catsetrealloc(dst, src->nbitpiece);
+	else
+		memset(dst->bitpiece, 0, dst->nbitpiece);
+	memcpy(dst->bitpiece, src->bitpiece, src->nbitpiece);
+	dst->nbitpiece = src->nbitpiece;
+	if (debug) fprint(2, "-> ");
+	if (debug) catsetprint(2, dst);
+	if (debug) fprint(2, "\n");
+}
+
+void
+catsetset(Catset*cs, int v)
+{
+	int p = v / 8;
+	int b = v % 8;
+	if (debug) fprint(2, "catsetset %p %d ", cs, v);
+	if (debug) catsetprint(2, cs);
+	cs->bitpiece[p] = 1 << b;
+	if (debug) fprint(2, "-> ");
+	if (debug) catsetprint(2, cs);
+	if (debug) fprint(2, "\n");
+}
+
+int
+catsetisset(Catset*cs)
+{
+	int i;
+
+	if (debug) fprint(2, "catsetisset %p ", cs);
+	if (debug) catsetprint(2, cs);
+	if (debug) fprint(2, "\n");
+	for (i =0; i < cs->nbitpiece; i++) {
+		if (cs->bitpiece[i])
+			return 1;
+	}
+	return 0;
+}
+
+void
+catsetorset(Catset*dst, Catset*src)
+{
+	int i;
+
+	if (debug) fprint(2, "catsetorset %p %p ", dst, src);
+	if (debug) catsetprint(2, dst);
+	if (debug) fprint(2, " ");
+	if (debug) catsetprint(2, src);
+	if (src->nbitpiece > dst->nbitpiece)
+		catsetrealloc(dst, src->nbitpiece);
+
+	for (i =0; i < src->nbitpiece; i++) {
+		dst->bitpiece[i] |= src->bitpiece[i];
+	}
+	if (debug) fprint(2, "-> ");
+	if (debug) catsetprint(2, dst);
+	if (debug) fprint(2, "\n");
+}
+
+int
+catseteq(Catset*cs1, Catset*cs2)
+{
+	int i;
+	Catset *css, * csl;
+
+	if (debug) fprint(2, "catseteq %p %p ", cs1, cs2);
+	if (debug) catsetprint(2, cs1);
+	if (debug) fprint(2, " ");
+	if (debug) catsetprint(2, cs2);
+	if (debug) fprint(2, "\n");
+	if (cs1->nbitpiece > cs2->nbitpiece) {
+		csl = cs1;
+		css = cs2;
+	} else {
+		csl = cs2;
+		css = cs1;
+	}
+	for (i =0; i < css->nbitpiece; i++) {
+		if (css->bitpiece[i] != csl->bitpiece[i])
+			return 0;
+	}
+	for (i = css->nbitpiece; i < csl->nbitpiece; i++) {
+		if (csl->bitpiece[i])
+			return 0;
+	}
+	return 1;
+}

+ 8 - 0
sys/src/games/music/jukefs/catset.h

@@ -0,0 +1,8 @@
+void catsetrealloc(Catset*, int);
+void catsetinit(Catset*, int);
+void catsetfree(Catset*t);
+void catsetcopy(Catset*, Catset*);
+void catsetset(Catset*, int);
+int catsetisset(Catset*);
+void catsetorset(Catset*, Catset*);
+int catseteq(Catset*, Catset*);

+ 757 - 0
sys/src/games/music/jukefs/fs.c

@@ -0,0 +1,757 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <bio.h>
+#include <fcall.h>
+#include "object.h"
+
+extern int debug;
+
+extern int mfd[];
+
+enum {
+	DbgFs = 0x1000
+};
+
+typedef struct Fid Fid;
+
+enum {
+	Busy =	0x01,
+	Open =	0x02,
+	Endf =	0x04,
+};
+
+struct Fid
+{
+	QLock;
+	Qid	qid;
+	int	fid;
+	ushort	flags;
+	vlong	offset;		// offset of data[0]
+	Fid	*next;
+};
+
+Fcall	thdr;
+Fcall	rhdr;
+
+enum {
+	/* Files making up an object */
+	Qchildren,		/* Each of these must be in dirtab */
+	Qdigest,
+	Qfiles,
+	Qfulltext,
+	Qkey,
+	Qminiparentage,
+	Qparent,
+	Qparentage,
+	Qtext,
+	Qtype,
+
+	/* Other files */
+	Qtop,	/* Must follow Qtype */
+	Qclassical,
+	Qdir,
+	Qroot,
+	Qctl,
+};
+
+#define PATH(id, f)	(((id)<<8) | (f))
+#define FILE(p)		((p) & 0xff)
+#define NUM(p)		((p) >> 8)
+
+char *dirtab[] =
+{
+[Qchildren]	"children",
+[Qdigest]	"digest",
+[Qdir]		".",
+[Qfiles]	"files",
+[Qfulltext]	"fulltext",
+[Qkey]		"key",
+[Qminiparentage]"miniparentage",
+[Qparent]	"parent",
+[Qparentage]	"parentage",
+[Qtext]		"text",
+[Qtype]		"type",
+[Qtop]		nil,
+};
+
+char	*rflush(Fid*), *rauth(Fid*),
+	*rattach(Fid*), *rwalk(Fid*),
+	*ropen(Fid*), *rcreate(Fid*),
+	*rread(Fid*), *rwrite(Fid*), *rclunk(Fid*),
+	*rremove(Fid*), *rstat(Fid*), *rwstat(Fid*),
+	*rversion(Fid*);
+
+char 	*(*fcalls[])(Fid*) = {
+	[Tflush]	rflush,
+	[Tversion]	rversion,
+	[Tauth]		rauth,
+	[Tattach]	rattach,
+	[Twalk]		rwalk,
+	[Topen]		ropen,
+	[Tcreate]	rcreate,
+	[Tread]		rread,
+	[Twrite]	rwrite,
+	[Tclunk]	rclunk,
+	[Tremove]	rremove,
+	[Tstat]		rstat,
+	[Twstat]	rwstat,
+};
+
+int	messagesize = 8*1024+IOHDRSZ;
+uchar	mdata[8*1024+IOHDRSZ];
+uchar	mbuf[8*1024+IOHDRSZ];
+char	bigbuf[1<<23];	/* 8 megabytes */
+Fid	*fids;
+
+char	Eperm[] =	"permission denied";
+char	Enotdir[] =	"not a directory";
+char	Enoauth[] =	"no authentication required";
+char	Enotexist[] =	"file does not exist";
+char	Einuse[] =	"file in use";
+char	Eexist[] =	"file exists";
+char	Enotowner[] =	"not owner";
+char	Eisopen[] = 	"file already open for I/O";
+char	Excl[] = 	"exclusive use file already open";
+char	Ename[] = 	"illegal name";
+char	Ebadctl[] =	"unknown control message";
+
+Fid *newfid(int fid);
+
+static int
+lookup(char *cmd, char *list[])
+{
+	int i;
+
+	for (i = 0; list[i] != nil; i++)
+		if (strcmp(cmd, list[i]) == 0)
+			return i;
+	return -1;
+}
+
+char*
+rversion(Fid *)
+{
+	Fid *f;
+
+	if(thdr.msize < 256)
+		return "max messagesize too small";
+	if(thdr.msize < messagesize)
+		messagesize = thdr.msize;
+	rhdr.msize = messagesize;
+	if(strncmp(thdr.version, "9P2000", 6) != 0)
+		return "unknown 9P version";
+	else
+		rhdr.version = "9P2000";
+	for(f = fids; f; f = f->next)
+		if(f->flags & Busy)
+			rclunk(f);
+	return nil;
+}
+
+char*
+rauth(Fid*)
+{
+	return Enoauth;
+}
+
+char*
+rflush(Fid *)
+{
+	return 0;
+}
+
+char*
+rattach(Fid *f)
+{
+	f->flags |= Busy;
+	f->qid.type = QTDIR;
+	f->qid.vers = 0;
+	f->qid.path = PATH(0, Qtop);
+	rhdr.qid = f->qid;
+	return 0;
+}
+
+static Fid*
+doclone(Fid *f, int nfid)
+{
+	Fid *nf;
+
+	nf = newfid(nfid);
+	nf->qid = f->qid;
+	if(nf->flags & Busy)
+		return nil;
+	nf->flags |= Busy;
+	nf->flags &= ~Open;
+	return nf;
+}
+
+char*
+dowalk(Fid *f, char *name)
+{
+	int t, n, m;
+	char *rv, *p;
+
+	t = FILE(f->qid.path);	/* Type */
+
+	rv = Enotexist;
+
+	if(strcmp(name, ".") == 0 && f->qid.type == QTDIR)
+		return nil;
+	if(strcmp(name, "..") == 0){
+		switch(t){
+		case Qtop:
+		case Qclassical:
+			f->qid.path = PATH(0, Qtop);
+			f->qid.type = QTDIR;
+			f->qid.vers = 0;
+			rv = nil;
+			break;
+		case Qdir:
+		case Qroot:
+			f->qid.path = PATH(0, Qclassical);
+			f->qid.type = QTDIR;
+			f->qid.vers = 0;
+			rv = nil;
+			break;
+		}
+		return rv;
+	}
+	switch(t){
+	case Qtop:
+		/* Contains classical */
+		if(strcmp(name, "juke") == 0){
+			f->qid.path = PATH(root->tabno, Qclassical);
+			f->qid.type = QTDIR;
+			f->qid.vers = 0;
+			rv = nil;
+			break;
+		}
+		break;
+	case Qclassical:
+		/* main dir, contains `root' and object dirs */
+		if(strcmp(name, "root") == 0){
+			f->qid.path = PATH(root->tabno, Qroot);
+			f->qid.type = QTDIR;
+			f->qid.vers = 0;
+			rv = nil;
+			break;
+		}
+		if(strcmp(name, "ctl") == 0){
+			f->qid.path = PATH(root->tabno, Qctl);
+			f->qid.type = QTFILE;
+			f->qid.vers = 0;
+			rv = nil;
+			break;
+		}
+		n = strtol(name, &p, 0);
+		if(*p)
+			break;	/* Not a number */
+		if(n < 0 || n >= notab)
+			break;	/* Outside range */
+		if(otab[n] == nil)
+			break;	/* Not in object table */
+		f->qid.path = PATH(n, Qdir);
+		f->qid.type = QTDIR;
+		f->qid.vers = 0;
+		rv = nil;
+		break;
+	case Qroot:	/* Root of the object hierarchy */
+	case Qdir:	/* Object directory */
+		if((m = lookup(name, dirtab)) < 0)
+			break;
+		n = NUM(f->qid.path);
+		f->qid.path = PATH(n, m);
+		f->qid.type = QTFILE;
+		f->qid.vers = 0;
+		rv = nil;
+		break;
+	}
+	return rv;
+}
+
+char*
+rwalk(Fid *f)
+{
+	Fid *nf;
+	char *rv;
+	int i;
+
+	if(f->flags & Open)
+		return Eisopen;
+
+	rhdr.nwqid = 0;
+	nf = nil;
+
+	/* clone if requested */
+	if(thdr.newfid != thdr.fid){
+		nf = doclone(f, thdr.newfid);
+		if(nf == nil)
+			return "new fid in use";
+		f = nf;
+	}
+
+	/* if it's just a clone, return */
+	if(thdr.nwname == 0 && nf != nil)
+		return nil;
+
+	/* walk each element */
+	rv = nil;
+	for(i = 0; i < thdr.nwname; i++){
+		rv = dowalk(f, thdr.wname[i]);
+		if(rv != nil){
+			if(nf != nil)	
+				rclunk(nf);
+			break;
+		}
+		rhdr.wqid[i] = f->qid;
+	}
+	rhdr.nwqid = i;
+
+	/* we only error out if no walk  */
+	if(i > 0)
+		rv = nil;
+
+	return rv;
+}
+
+char *
+ropen(Fid *f)
+{
+	if(f->flags & Open)
+		return Eisopen;
+
+	if(thdr.mode != OREAD && FILE(f->qid.path) != Qctl)
+		return Eperm;
+	rhdr.iounit = 0;
+	rhdr.qid = f->qid;
+	f->flags |= Open;
+	f->flags &= ~Endf;
+	return nil;
+}
+
+char *
+rcreate(Fid*)
+{
+	return Eperm;
+}
+
+static long
+fileinfo(char *buf, int bufsize, int onum, int t)
+{
+	long n;
+
+	n = 0;
+	switch(t){
+	case Qchildren:
+		n = printchildren(buf, bufsize, otab[onum]);
+		break;
+	case Qdigest:
+		n = printdigest(buf, bufsize, otab[onum]);
+		break;
+	case Qfulltext:
+		n = printfulltext(buf, bufsize, otab[onum]);
+		break;
+	case Qkey:
+		n = printkey(buf, bufsize, otab[onum]);
+		break;
+	case Qparent:
+		n = printparent(buf, bufsize, otab[onum]);
+		break;
+	case Qtext:
+		n = printtext(buf, bufsize, otab[onum]);
+		break;
+	case Qtype:
+		n = printtype(buf, bufsize, otab[onum]);
+		break;
+	case Qfiles:
+		n = printfiles(buf, bufsize, otab[onum]);
+		break;
+	case Qparentage:
+		n = printparentage(buf, bufsize, otab[onum]);
+		break;
+	case Qminiparentage:
+		n = printminiparentage(buf, bufsize, otab[onum]);
+		break;
+	default:
+		sysfatal("rread: %d", t);
+	}
+	return n;
+}
+
+static void
+mkstat(Dir *d, int n, int t)
+{
+	static char buf[16];
+
+	d->uid = user;
+	d->gid = user;
+	d->muid = user;
+	d->qid.vers = 0;
+	d->qid.type = QTFILE;
+	d->type = 0;
+	d->dev = 0;
+	d->atime = time(0);
+	d->mtime = d->atime;
+	switch(t){
+	case Qtop:
+		d->name = ".";
+		d->mode = DMDIR|0555;
+		d->atime = d->mtime = time(0);
+		d->length = 0;
+		d->qid.path = PATH(0, Qtop);
+		d->qid.type = QTDIR;
+		break;
+	case Qclassical:
+		d->name = "juke";
+		d->mode = DMDIR|0555;
+		d->atime = d->mtime = time(0);
+		d->length = 0;
+		d->qid.path = PATH(0, Qclassical);
+		d->qid.type = QTDIR;
+		break;
+	case Qdir:
+		snprint(buf, sizeof buf, "%d", n);
+		d->name = buf;
+		d->mode = DMDIR|0555;
+		d->length = 0;
+		d->qid.path = PATH(n, Qdir);
+		d->qid.type = QTDIR;
+		break;
+	case Qroot:
+		d->name = "root";
+		d->mode = DMDIR|0555;
+		d->length = 0;
+		d->qid.path = PATH(0, Qroot);
+		d->qid.type = QTDIR;
+		break;
+	case Qctl:
+		d->name = "ctl";
+		d->mode = 0666;
+		d->length = 0;
+		d->qid.path = PATH(0, Qctl);
+		break;
+		d->name = "ctl";
+		d->mode = 0666;
+		d->length = 0;
+		d->qid.path = PATH(0, Qctl);
+		break;
+	case Qchildren:
+	case Qdigest:
+	case Qfiles:
+	case Qfulltext:
+	case Qkey:
+	case Qminiparentage:
+	case Qparent:
+	case Qparentage:
+	case Qtext:
+	case Qtype:
+		d->name = dirtab[t];
+		d->mode = 0444;
+		d->length = fileinfo(bigbuf, sizeof bigbuf, n, t);
+		d->qid.path = PATH(n, t);
+		break;
+	default:
+		sysfatal("mkstat: %d", t);
+	}
+}
+
+int
+readtopdir(Fid*, uchar *buf, long off, int cnt, int blen)
+{
+	int m, n;
+	Dir d;
+
+	n = 0;
+	mkstat(&d, 0, Qclassical);
+	m = convD2M(&d, &buf[n], blen);
+	if(off <= 0){
+		if(m <= BIT16SZ || m > cnt)
+			return n;
+		n += m;
+	}
+	return n;
+}
+
+int
+readclasdir(Fid*, uchar *buf, long off, int cnt, int blen)
+{
+	int m, n;
+	long pos;
+	Dir d;
+	Fid *fid;
+
+	n = 0;
+	pos = 0;
+	mkstat(&d, 0, Qctl);
+	m = convD2M(&d, &buf[n], blen);
+	if(off <= pos){
+		if(m <= BIT16SZ || m > cnt)
+			return 0;
+		n += m;
+		cnt -= m;
+	}
+	pos += m;
+	mkstat(&d, 0, Qroot);
+	m = convD2M(&d, &buf[n], blen);
+	if(off <= pos){
+		if(m <= BIT16SZ || m > cnt)
+			return n;
+		n += m;
+		cnt -= m;
+	}
+	pos += m;
+	for (fid = fids; fid; fid = fid->next){
+		if(FILE(fid->qid.path) != Qdir)
+			continue;
+		mkstat(&d, NUM(fid->qid.path), Qdir);
+		m = convD2M(&d, &buf[n], blen-n);
+		if(off <= pos){
+			if(m <= BIT16SZ || m > cnt)
+				break;
+			n += m;
+			cnt -= m;
+		}
+		pos += m;
+	}
+	return n;
+}
+
+int
+readdir(Fid *f, uchar *buf, long off, int cnt, int blen)
+{
+	int i, m, n;
+	long pos;
+	Dir d;
+
+	n = 0;
+	pos = 0;
+	for (i = 0; i < Qtop; i++){
+		mkstat(&d, NUM(f->qid.path), i);
+		m = convD2M(&d, &buf[n], blen-n);
+		if(off <= pos){
+			if(m <= BIT16SZ || m > cnt)
+				break;
+			n += m;
+			cnt -= m;
+		}
+		pos += m;
+	}
+	return n;
+}
+
+void
+readbuf(char *s, long n)
+{
+	rhdr.count = thdr.count;
+	if(thdr.offset >= n){
+		rhdr.count = 0;
+		return;
+	}
+	if(thdr.offset+rhdr.count > n)
+		rhdr.count = n - thdr.offset;
+	rhdr.data = s + thdr.offset;
+}
+
+char*
+rread(Fid *f)
+{
+	long off;
+	int n, cnt, t;
+
+	rhdr.count = 0;
+	off = thdr.offset;
+	cnt = thdr.count;
+
+	if(cnt > messagesize - IOHDRSZ)
+		cnt = messagesize - IOHDRSZ;
+
+	rhdr.data = (char*)mbuf;
+
+	n = 0;
+	t = FILE(f->qid.path);
+	switch(t){
+	case Qtop:
+		n = readtopdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+		rhdr.count = n;
+		return nil;
+	case Qclassical:
+		n = readclasdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+		rhdr.count = n;
+		return nil;
+	case Qdir:
+	case Qroot:
+		n = readdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+		rhdr.count = n;
+		return nil;
+	case Qctl:
+		snprint(bigbuf, sizeof bigbuf, "%d objects in tree (%d holes)\n", notab, hotab);
+		break;
+	case Qchildren:
+	case Qdigest:
+	case Qfiles:
+	case Qfulltext:
+	case Qkey:
+	case Qminiparentage:
+	case Qparent:
+	case Qparentage:
+	case Qtext:
+	case Qtype:
+		n = fileinfo(bigbuf, sizeof bigbuf, NUM(f->qid.path), t);
+		break;
+	default:
+		sysfatal("rread: %d", t);
+	}
+	readbuf(bigbuf, n);
+	return nil;
+}
+
+char*
+rwrite(Fid *f)
+{
+	long cnt;
+	char *p;
+
+	if(FILE(f->qid.path) != Qctl)
+		return Eperm;
+	rhdr.count = 0;
+	cnt = thdr.count;
+	if(p = strchr(thdr.data, '\n'))
+		*p = '\0';
+	if(strncmp(thdr.data, "quit", cnt) == 0)
+		threadexitsall(nil);
+	else if(strncmp(thdr.data, "reread", cnt) == 0)
+		reread();
+	else
+		return "illegal command";
+	rhdr.count = thdr.count;
+	return nil;
+}
+
+char *
+rclunk(Fid *f)
+{
+	f->flags &= ~(Open|Busy);
+	return 0;
+}
+
+char *
+rremove(Fid *)
+{
+	return Eperm;
+}
+
+char *
+rstat(Fid *f)
+{
+	Dir d;
+
+	mkstat(&d, NUM(f->qid.path), FILE(f->qid.path));
+	rhdr.nstat = convD2M(&d, mbuf, messagesize - IOHDRSZ);
+	rhdr.stat = mbuf;
+	return 0;
+}
+
+char *
+rwstat(Fid*)
+{
+	return Eperm;
+}
+
+Fid *
+newfid(int fid)
+{
+	Fid *f, *ff;
+
+	ff = nil;
+	for(f = fids; f; f = f->next)
+		if(f->fid == fid){
+			return f;
+		}else if(ff == nil && (f->flags & Busy) == 0)
+			ff = f;
+	if(ff == nil){
+		ff = malloc(sizeof *ff);
+		if (ff == nil)
+			sysfatal("malloc: %r");
+		memset(ff, 0, sizeof *ff);
+		ff->next = fids;
+		fids = ff;
+	}
+	ff->fid = fid;
+	return ff;
+}
+
+void
+io(void *)
+{
+	char *err, e[32];
+	int n;
+	extern int p[];
+	Fid *f;
+
+	threadsetname("file server");
+	close(p[1]);
+	for(;;){
+		/*
+		 * reading from a pipe or a network device
+		 * will give an error after a few eof reads
+		 * however, we cannot tell the difference
+		 * between a zero-length read and an interrupt
+		 * on the processes writing to us,
+		 * so we wait for the error
+		 */
+		n = read9pmsg(mfd[0], mdata, messagesize);
+		if(n == 0)
+			continue;
+		if(n < 0){
+			rerrstr(e, sizeof e);
+			if (strcmp(e, "interrupted") == 0){
+				if (debug & DbgFs) fprint(2, "read9pmsg interrupted\n");
+				continue;
+			}
+			return;
+		}
+		if(convM2S(mdata, n, &thdr) == 0)
+			continue;
+
+		if(debug & DbgFs)
+			fprint(2, "io:<-%F\n", &thdr);
+
+		rhdr.data = (char*)mbuf;
+
+		if(!fcalls[thdr.type])
+			err = "bad fcall type";
+		else {
+			f = newfid(thdr.fid);
+			err = (*fcalls[thdr.type])(f);
+		}
+		if(err){
+			rhdr.type = Rerror;
+			rhdr.ename = err;
+		}else{
+			rhdr.type = thdr.type + 1;
+			rhdr.fid = thdr.fid;
+		}
+		rhdr.tag = thdr.tag;
+		if(debug & DbgFs)
+			fprint(2, "io:->%F\n", &rhdr);/**/
+		n = convS2M(&rhdr, mdata, messagesize);
+		if(write(mfd[1], mdata, n) != n)
+			sysfatal("mount write");
+	}
+	threadexitsall("die yankee pig dog");
+}
+
+int
+newid(void)
+{
+	int rv;
+	static int id;
+	static Lock idlock;
+
+	lock(&idlock);
+	rv = ++id;
+	unlock(&idlock);
+
+	return rv;
+}

+ 6 - 0
sys/src/games/music/jukefs/mk.dep

@@ -0,0 +1,6 @@
+catset.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/bio.h object.h catset.h
+fs.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/bio.h /sys/include/fcall.h object.h
+parse.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/bio.h /sys/include/ctype.h object.h catset.h parse.h
+print.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/ctype.h /sys/include/bio.h /sys/include/thread.h object.h parse.h catset.h
+search.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/bio.h /sys/include/thread.h object.h parse.h search.h
+server.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/bio.h /sys/include/fcall.h object.h parse.h print.h catset.h

+ 18 - 0
sys/src/games/music/jukefs/mkfile

@@ -0,0 +1,18 @@
+</$objtype/mkfile
+<../mkinc
+
+CFLAGS = -DDEFAULTMAP="$DEFAULTMAP"
+TARG = jukefs
+BIN = /$objtype/bin/games
+
+CFILES=\
+	catset.c\
+	fs.c\
+	parse.c\
+	print.c\
+	search.c\
+	server.c\
+
+OFILES = ${CFILES:%.c=%.$O}
+
+</sys/src/cmd/mkone

+ 117 - 0
sys/src/games/music/jukefs/object.h

@@ -0,0 +1,117 @@
+/* Keywords */
+
+typedef enum {
+	Category,
+	Cddata,
+	Cmd,
+	File,
+	Include,
+	Key,
+	Lyrics,
+	Part,
+	Path,
+	Performance,
+	Recording,
+	Root,
+	Search,
+	Soloists,
+	Time,
+	Track,
+	Work,
+	Ntoken,	/* Initializer for ntoken */
+	Eof	=	-1,
+	Txt	=	-2,
+	BraceO	=	-3,
+	BraceC	=	-4,
+	Equals	=	-5,
+	Newcat	=	-6,
+} Type;
+
+typedef struct Object Object;
+typedef struct Catset Catset;
+typedef struct Token Token;
+typedef struct Cmdlist Cmdlist;
+
+/* Token-only types */
+
+typedef enum {
+	Obj,
+	Cat,
+} Kind;
+
+struct Catset {
+	uchar *bitpiece;	/* mallocated */
+	int nbitpiece;
+};
+
+
+struct Token {
+	char	*name;
+	Kind	kind;
+	long	value;
+	char	*kname;
+	Catset	categories;
+};
+
+typedef enum {
+	Hierarchy,
+	Typelist,
+	Nlisttype,
+} Listtype;
+
+struct Cmdlist {
+	int	flag;
+	char	*name;
+};
+
+#define KEYLEN 128
+
+struct Object {
+	Type	type;
+	int	tabno;		/* entry in object table */
+	Object	*parent;
+	Object	**children;	/* mallocated */
+	Object	**catparents;
+	Object	*orig;		/* back pointer from search object */
+	int	nchildren;
+	int	ncatparents;
+	Catset	categories;	/* was int */
+	int	flags;
+	int	num;		/* for enumerations */
+	char	*value;		/* mallocated */
+	char	key[KEYLEN];
+	char	*path;		/* mallocated */
+};
+
+#define Sort	0x01
+#define Enum	0x02
+#define Hier	0x04
+#define Elab	0x10	/* elaborated rune string */
+
+extern	Token	*tokenlist;
+extern	int	ncat;
+extern	Object	**catobjects;
+extern	Biobuf	*f;
+extern	char	file[];
+extern	Object	*root;
+extern	int	ntoken;
+
+extern	Object	**otab;	// object table
+extern	int	notab;	// no of entries used
+extern	int	sotab;	// no of entries mallocated
+extern	int	hotab;	// no of holes in tab
+extern	char	*user;
+
+void	io(void *);
+long	printchildren(char*, int, Object*);
+long	printdigest(char*, int, Object*);
+long	printfiles(char*, int, Object*);
+long	printfulltext(char*, int, Object*);
+long	printkey(char*, int, Object*);
+long	printminiparentage(char*, int, Object*);
+long	printparent(char*, int, Object*);
+long	printparentage(char*, int, Object*);
+long	printtext(char*, int, Object*);
+long	printtype(char*, int, Object*);
+void	reread(void);
+void	listfiles(Object *o);

+ 614 - 0
sys/src/games/music/jukefs/parse.c

@@ -0,0 +1,614 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <bio.h>
+#include <ctype.h>
+#include "object.h"
+#include "catset.h"
+#include "parse.h"
+
+#define MAXTOKEN 1024
+
+Biobuf *f;
+static int str;
+char file[256];
+
+Token tokenlistinit[] = {
+	{ "category",	Obj,	Category	, "music"	, {nil,0}},
+	{ "cddata",	Obj,	Cddata		, nil		, {nil,0}},
+	{ "command",	Obj,	Cmd		, nil		, {nil,0}},
+	{ "file",	Obj,	File		, "file"	, {nil,0}},
+	{ "include",	Obj,	Include		, nil		, {nil,0}},
+	{ "key",	Obj,	Key		, nil		, {nil,0}},
+	{ "lyrics",	Obj,	Lyrics		, "lyrics"	, {nil,0}},
+	{ "part",	Obj,	Part		, "title"	, {nil,0}},
+	{ "path",	Obj,	Path		, nil		, {nil,0}},
+	{ "performance",Obj,	Performance	, "artist"	, {nil,0}},
+	{ "recording",	Obj,	Recording	, "title"	, {nil,0}},
+	{ "root",	Obj,	Root		, nil		, {nil,0}},
+	{ "search",	Obj,	Search		, nil		, {nil,0}},
+	{ "soloists",	Obj,	Soloists	, "artist"	, {nil,0}},
+	{ "time",	Obj,	Time		, "time"	, {nil,0}},
+	{ "track",	Obj,	Track		, "title"	, {nil,0}},
+	{ "work",	Obj,	Work		, "title"	, {nil,0}},
+};
+Token *tokenlist;
+int ntoken = Ntoken;
+int catnr = 0;
+
+Cmdlist cmdlist[] = {
+	{	Sort,	"sort"		},
+	{	Enum,	"number"	},
+	{	0x00,	0		},
+};
+
+static char *curtext;
+
+void
+inittokenlist(void)
+{
+	int i;
+
+	tokenlist = malloc(ntoken*sizeof(Token));
+	memmove(tokenlist, tokenlistinit, ntoken*sizeof(Token));
+	for(i = 0; i< Ntoken; i++){
+		tokenlist[i].name = strdup(tokenlist[i].name);
+		catsetinit(&tokenlist[i].categories, tokenlist[i].value);
+	}
+	curtext = smprint("{");
+}
+
+Type
+gettoken(char *token)
+{
+	char *p, *q;
+	int i, n;
+	Token *t;
+
+	for(;;){
+		if(curtext){
+			p = &curtext[strspn(curtext, " \t")];	
+			if(*p && *p != '\n')
+				break;
+		}
+		do {
+			str++;
+			free(curtext);
+			if((curtext = Brdstr(f, '\n', 0)) == nil)
+				return Eof;
+		} while(curtext[0] == '#');
+	}
+	if(*p == '{'){
+		*token++ = *p;
+		*token = 0;
+		*p = ' ';
+		return BraceO;
+	}
+	if(*p == '}'){
+		*token++ = *p;
+		*token = 0;
+		*p = ' ';
+		return BraceC;
+	}
+	if(*p == '='){
+		*token++ = *p;
+		*token = 0;
+		*p = ' ';
+		return Equals;
+	}
+	t = nil;
+	n = 0;
+	for(i = 0; i < ntoken; i++){
+		t = &tokenlist[i];
+		if(strncmp(p, t->name, n=strlen(t->name)) == 0){
+			q = &p[n];
+				if(isalnum(*q) || *q == '-') continue;
+			q += strspn(q, " \t");
+			if(t->kind == Obj && *q == '{')
+				break;
+			if(t->kind == Cat && *q == '=')
+				break;
+		}
+	}
+	if(i < ntoken){
+		strcpy(token, t->name);
+		memset(p, ' ', n);
+		return i;
+	}
+	assert(strlen(token) < MAXTOKEN);
+	if(strchr(p, '{'))
+		sysfatal("Illegal keyword or parse error: %s", p);
+	if((q = strchr(p, '='))){
+		if(q == p) goto tx;
+		*q = 0;
+		strcpy(token, p);
+		assert(strlen(token) < MAXTOKEN);
+		memset(p, ' ', q-p);
+		*q = '=';
+		for(q = token; *q; q++)
+			if(!isalnum(*q) && !isspace(*q)) break;
+		if(*q) return Txt;
+		while(isspace(*--q)) *q = 0;
+		return Newcat;
+	}
+tx:	if((q = strchr(p, '}'))){
+		*q = 0;
+		strcpy(token, p);
+		assert(strlen(token) < MAXTOKEN);
+		memset(p, ' ', q-p);
+		*q = '}';
+		return Txt;
+	}
+	strcpy(token, p);
+	assert(strlen(token) < MAXTOKEN);
+	free(curtext);
+	curtext = nil;
+	return Txt;
+}
+
+Object *
+getobject(Type t, Object *parent)
+{
+	char *token;
+	char *textbuf;
+	char *tp, *p, *q;
+	int i;
+	Object *o, *oo, *child;
+	Token *ot;
+	Type nt;
+
+	token = malloc(MAXTOKEN);
+	textbuf = malloc(8192);
+
+	tp = textbuf;
+	o = newobject(t, parent);
+	o->flags |= Hier;
+	if(parent == nil){
+		root = o;
+		o->path = strdup(startdir);
+		setmalloctag(o->path, 0x100001);
+	}
+	if(gettoken(token) != BraceO)
+		sysfatal("Parse error: no brace, str %d", str);
+	for(;;){
+		t = gettoken(token);
+		if(t >= 0)
+			switch(tokenlist[t].kind){
+			case Obj:
+				switch(t){
+				case Key:
+				case Cmd:
+				case Path:
+					if(getobject(t, o) != nil)
+						sysfatal("Non-null child?");
+					break;
+				case Include:
+				case Category:
+					child = getobject(t, o);
+					if(child) addchild(o, child, "case Category");
+					break;
+				default:
+					/* subobject */
+					child = getobject(t, o);
+					if(child == nil)
+						sysfatal("Null child?");
+					addchild(o, child, "default");
+					break;
+				}
+				break;
+			case Cat:
+			catcase:    nt = gettoken(token);
+				if(nt != Equals)
+					sysfatal("Expected Equals, not %s", token);
+				nt = gettoken(token);
+				if(nt != Txt)
+					sysfatal("Expected Text, not %s", token);
+				if((p = strchr(token, '\n'))) *p = 0;
+				p = token;
+				if(o->type == Category){
+					if(catsetisset(&o->categories)){
+						fprint(2, "Category object must have one category\n");
+					}
+					catsetcopy(&o->categories, &tokenlist[t].categories);
+					strncpy(o->key, p, KEYLEN);
+					if(catobjects[t] == 0)
+						sysfatal("Class %s not yet defined", tokenlist[t].name);
+					for(i = 0; i < catobjects[t]->nchildren; i++)
+						if(strcmp(catobjects[t]->children[i]->key, p) == 0)
+							break;
+					if(i == catobjects[t]->nchildren){
+						/* It's a new key for the category */
+						addchild(catobjects[t], o, "new key for cat");
+					}else{
+						/* Key already existed */
+						oo = catobjects[t]->children[i];
+						if(oo->value)
+							sysfatal("Duplicate category object for %s", oo->value);
+						catobjects[t]->children[i] = o;
+						if(oo->nchildren){
+							for(i = 0; i < oo->nchildren; i++){
+								if(oo->children[i]->parent == oo)
+									oo->children[i]->parent = o;
+								addchild(o, oo->children[i], "key already existed");
+							}
+						}
+						freeobject(oo, "a");
+					}
+					o->parent = catobjects[t];
+				}else{
+					catsetorset(&o->categories, &tokenlist[t].categories);
+					for(i = 0; i < catobjects[t]->nchildren; i++)
+						if(strcmp(catobjects[t]->children[i]->key, p) == 0)
+							break;
+					if(i == catobjects[t]->nchildren){
+						oo = newobject(Category, catobjects[t]);
+/*
+						oo->value = strdup(token);
+*/
+						strncpy(oo->key, p, KEYLEN);
+						catsetcopy(&oo->categories, &tokenlist[t].categories);
+						addchild(catobjects[t], oo, "catobjects[t],oo");
+					}
+					addchild(catobjects[t]->children[i], o, "children[i]");
+				}
+				break;
+			}
+		else
+			switch(t){
+			case Eof:
+				if(o->type == Root){
+					free(token);
+					free(textbuf);
+					return o;
+				}
+				sysfatal("Unexpected Eof in %s, file %s", tokenlist[o->type].name, file);
+			case Newcat:
+				/* New category, make an entry in the tokenlist */
+				tokenlist = realloc(tokenlist, (ntoken+1)*sizeof(Token));
+				ot = &tokenlist[ntoken];
+				ot->name = strdup(token);
+				setmalloctag(ot->name, 0x100002);
+				ot->kind = Cat;
+				ot->value = -1;
+				memset(&ot->categories, 0, sizeof(Catset));
+				catsetinit(&ot->categories, catnr++);
+				/* And make an entry in the catobjects table */
+				if(ncat <= ntoken){
+					catobjects = realloc(catobjects, (ntoken+1)*sizeof(Object*));
+					while(ncat <= ntoken) catobjects[ncat++] = nil;
+				}
+				if(catobjects[ntoken] != nil)
+					sysfatal("Class %s already defined in %s:%d", token, file, str);
+				if(0) fprint(2, "newcat: token %s catnr %d ntoken %d ncat %d\n",
+					token, catnr, ntoken, ncat);
+				catobjects[ntoken] = newobject(Category, root);
+				if(o->type == Category)
+					catobjects[ntoken]->flags = o->flags&Hier;
+				catobjects[ntoken]->flags |= Sort;
+				strncpy(catobjects[ntoken]->key, token, KEYLEN);
+				catsetcopy(&catobjects[ntoken]->categories, &ot->categories);
+				addchild(root, catobjects[ntoken], "root");
+				t = ntoken;
+				ntoken++;
+				goto catcase;
+			case Txt:
+				strcpy(tp, token);
+				tp += strlen(token);
+				break;
+			case BraceC:
+				while(tp > textbuf && tp[-1] == '\n') *--tp = 0;
+				if((o->type == File || o->type == Include) && o->path){
+					o->value = smprint("%s/%s", o->path, textbuf);
+				}else if(tp > textbuf){
+					o->value = strdup(textbuf);
+					setmalloctag(o->value, 0x100003);
+				}
+				switch(o->type){
+				case Cmd:
+					q = strtok(o->value, " \t,;\n");
+					while(q){
+						if(*q) for(i = 0; cmdlist[i].name; i++){
+							if(strcmp(q, cmdlist[i].name) == 0){
+								o->parent->flags |= cmdlist[i].flag;
+								break;
+							}
+							if(cmdlist[i].name == 0)
+								fprint(2, "Unknown command: %s\n", q);
+						}
+						q = strtok(nil, " \t,;\n");
+					}
+					freeobject(o, "b");
+					free(token);
+					free(textbuf);
+					return nil;
+				case Path:
+					p = o->value;
+					free(o->parent->path);
+					if(p[0] == '/' || o->path == nil){
+						o->parent->path = strdup(p);
+						setmalloctag(o->parent->path, 0x100004);
+					}else{
+						o->parent->path = smprint("%s/%s", o->path, p);
+						setmalloctag(o->parent->path, 0x100005);
+					}
+					freeobject(o, "b");
+					free(token);
+					free(textbuf);
+					return nil;
+				case Include:
+					free(token);
+					free(textbuf);
+					return getinclude(o);
+				case Category:
+				/*
+					if(o->nchildren) break;
+				 */
+					free(token);
+					free(textbuf);
+					return nil;
+				case Key:
+					strncpy(o->parent->key, o->value, KEYLEN);
+					freeobject(o, "d");
+					free(token);
+					free(textbuf);
+					return nil;
+				default:
+					break;
+				}
+				free(token);
+				free(textbuf);
+				return o;
+			default:
+				fprint(2, "Unexpected token: %s\n", token);
+				free(token);
+				free(textbuf);
+				return nil;
+			}
+	}
+}
+
+Object *
+getinclude(Object *o)
+{
+		char *savetext;
+		Biobuf *savef = f;
+		char savefile[256], fname[256];
+		Object *oo;
+		int savestr = str;
+		char token[MAXTOKEN], *dirname, *filename;
+		Type t;
+
+		str = 0;
+		if(curtext){
+			savetext = strdup(curtext);
+			setmalloctag(savetext, 0x100006);
+		}else
+			savetext = nil;
+		strncpy(savefile, file, 256);
+		if((f = Bopen(o->value, OREAD)) == nil)
+			sysfatal("getinclude: %s: %r", o->value);
+		strncpy(file, o->value, 256);
+		strncpy(fname, o->value, 256);
+		if((filename = strrchr(fname, '/'))){
+			*filename = 0;
+			dirname = fname;
+			filename++;
+		}else{
+			dirname = "";
+			filename = fname;
+		}
+		while((t = gettoken(token)) != Eof){
+			if(t < 0){
+				if(*dirname)
+					sysfatal("Bad include file %s/%s, token %s, str %d",
+						dirname, filename, token, str);
+				else
+					sysfatal("Bad include file %s, token %s, str %d",
+						filename, token, str);
+			}
+			free(o->path);
+			o->path = strdup(dirname);
+			setmalloctag(o->path, 0x100007);
+			oo = getobject(t, o->parent);
+			if(oo) addchild(o->parent, oo, "o->parent, oo");
+		}
+		freeobject(o, "e");
+		free(curtext);
+		curtext = nil;
+		if(savetext)
+			curtext = savetext;
+		strncpy(file, savefile, 256);
+		str = savestr;
+		Bterm(f);
+		f = savef;
+		return nil;
+}
+
+void
+addchild(Object *parent, Object *child, char *where)
+{
+		int i;
+
+		/* First check if child's already been added
+		 * This saves checking elsewhere
+		 */
+		for(i = 0; i < parent->nchildren; i++)
+				if(parent->children[i] == child) return;
+		parent->children = realloc(parent->children, (i+1)*4);
+		parent->children[i] = child;
+		parent->nchildren++;
+		if(parent->type == Category && child->type == Category)
+			return;
+		if(parent->type == Work && child->type == Work)
+			return;
+		if(parent->type == Work && child->type == Track)
+			return;
+		if(parent->type == Track && child->type == File)
+			return;
+		if(child->parent == child)
+			return;
+		if(parent->type == Root)
+			return;
+		if(parent->parent->type == Root)
+			return;
+//		addcatparent(parent, child);
+		i = child->ncatparents;
+		if(0) fprint(2, "addcatparent %s parent %d type %d child %d type %d\n",where,
+			parent->tabno, parent->type, child->tabno, child->type);
+		child->catparents = realloc(child->catparents, (i+1)*4);
+		child->catparents[i] = parent;
+		child->ncatparents++;
+}
+
+void
+addcatparent(Object *parent, Object *child)
+{
+		int i;
+
+		/* First check if child's already been added
+		 * This saves checking elsewhere
+		 */
+		if(child->parent == child)
+			return;
+//		for(i = 0; i < child->ncatparents; i++)
+//				if(child->catparents[i] == parent) return;
+		i = child->ncatparents;
+		fprint(2, "addcatparent parent %d child %d\n", parent->tabno, child->tabno);
+		child->catparents = realloc(child->catparents, (i+1)*4);
+		child->catparents[i] = parent;
+		child->ncatparents++;
+}
+
+void
+sortprep(char *out, int n, Object *o)
+{
+	char *p, *q;
+
+	if(*o->key)
+		q = o->key;
+	else if (o->value)
+		q = o->value;
+	else
+		q = "";
+	if(p = strchr(q, '~'))
+		p++;
+	else
+		p = q;
+	for(q = out; *p && q < out+n-1; q++)
+		*q = tolower(*p++);
+	*q = 0;
+}
+
+void
+childsort(Object *o)
+{
+		Object *oo;
+		int i, j, n;
+		char si[256], sj[256];
+		/* sort the kids by key or by value */
+
+		n = o->nchildren;
+		if(n > 1){
+			for(i = 0; i < n-1; i++){
+				sortprep(si, nelem(si), o->children[i]);
+				for(j = i+1; j < n; j++){
+					sortprep(sj, nelem(sj), o->children[j]);
+					if(strncmp(si, sj, sizeof(si)) > 0){
+						oo = o->children[i];
+						o->children[i] = o->children[j];
+						o->children[j] = oo;
+						strncpy(si, sj, sizeof(si));
+					}
+				}
+			}
+		}
+}
+
+void
+childenum(Object *o){
+		Object *oo;
+		int i, n = 1;
+
+		for(i = 0; i < o->nchildren; i++){
+			oo = o->children[i];
+			if(tokenlist[oo->type].kind == Cat)
+				oo->num = n++;
+			else
+				switch(oo->type){
+				case Category:
+				case Part:
+				case Recording:
+				case Track:
+				case Work:
+					oo->num = n++;
+				default:
+					break;
+				}
+		}
+}
+
+Object *
+newobject(Type t, Object *parent){
+	Object *o;
+	int tabno;
+
+	if(hotab){
+		for(tabno = 0; tabno < notab; tabno++)
+			if(otab[tabno] == nil)
+				break;
+		if(tabno == notab)
+			sysfatal("lost my hole");
+		hotab--;
+	}else{
+		if(sotab < notab+1){
+			sotab += 512;
+			otab = realloc(otab, sizeof(Object*)*sotab);
+			if(otab == nil)
+				sysfatal("realloc: %r");
+		}
+		tabno = notab++;
+	}
+	o = mallocz(sizeof(Object), 1);
+	o->tabno = tabno;
+	otab[tabno] = o;
+	o->type = t;
+	o->parent = parent;
+	if(parent && parent->path){
+		o->path = strdup(parent->path);
+		setmalloctag(o->path, 0x100008);
+	}
+	return o;
+}
+
+void
+freeobject(Object *o, char*){
+
+	free(o->children);
+	if(o->orig == nil)
+		free(o->value);
+	free(o->path);
+	free(o->catparents);
+	catsetfree(&o->categories);
+	otab[o->tabno] = nil;
+	hotab++;
+	free(o);
+}
+
+void
+freetree(Object *o)
+{
+	int i;
+
+	for(i = 0; i < o->nchildren; i++)
+		if(o->children[i]->parent == o)
+			freetree(o->children[i]);
+	free(o->children);
+	if(o->orig == nil)
+		free(o->value);
+	free(o->path);
+	free(o->catparents);
+	catsetfree(&o->categories);
+	otab[o->tabno] = nil;
+	hotab++;
+	free(o);
+}

+ 15 - 0
sys/src/games/music/jukefs/parse.h

@@ -0,0 +1,15 @@
+Object	*getobject(Type, Object *);
+Object	*getinclude(Object *);
+void	childsort(Object *);
+void	childenum(Object *);
+Object	*newobject(Type, Object *);
+void	freeobject(Object *, char *);
+void	freetree(Object *);
+void	*mymalloc(void *old, int size);
+void	addchild(Object *, Object *, char*);
+void	addcatparent(Object *, Object *);
+void	inittokenlist(void);
+void	initparse(void);
+void	exit(int);
+
+extern char *startdir;

+ 489 - 0
sys/src/games/music/jukefs/print.c

@@ -0,0 +1,489 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <bio.h>
+#include <thread.h>
+#include "object.h"
+#include "parse.h"
+#include "catset.h"
+
+int	fflag;
+
+void
+listfiles(Object *o)
+{
+	int i;
+
+	if(o->type == File){
+		print("%s\n", o->value);
+		return;
+	}
+	for(i = 0; i < o->nchildren; i++)
+		if(o->children[i]->parent == o)
+			listfiles(o->children[i]);
+}
+
+int
+indent(char *lp, int ln, int n, char *buf) {
+	int sln;
+	char *p, c;
+
+	sln = ln;
+	if (ln <= 0)
+		return 0;
+	if (n < 0)
+		n = -n;
+	else {
+		if (ln < 4*n)
+			goto out;
+		memset(lp, ' ', 4*n);
+		lp += 4*n;
+		ln -= 4*n;
+	}
+	if(p = buf) while (ln > 1) {
+		c = *p++;
+		if(c == '\0')
+			break;
+		if(c == '~')
+			continue;
+		*lp++ = c;
+		ln--;
+		if (c == '\n' && p[1]) {
+			if (ln < 4*n)
+				break;
+			memset(lp, ' ', 4*n);
+			lp += 4*n;
+			ln -= 4*n;
+		}
+	}
+	*lp = '\0';
+out:
+	return sln - ln;
+}
+
+long
+printchildren(char *lp, int ln, Object *o) {
+	int i, r;
+	char *sp;
+
+	sp = lp;
+	if (o->flags & Sort) {
+		childsort(o);
+		o->flags &= ~Sort;
+	}
+	for(i = 0; i < o->nchildren && ln > 0; i++){
+		r = snprint(lp, ln, "%d\n", o->children[i]->tabno);
+		lp += r;
+		ln -= r;
+	}
+	return lp - sp;
+}
+
+long
+printminiparentage(char *lp, int ln, Object *o) {
+	char *p, c;
+	int r, sln;
+
+	if (ln <= 0) return 0;
+	*lp = '\0';
+	if (o == 0 || o->type == Root)
+		return 0;
+	sln = ln;
+	if(o->orig) o = o->orig;
+	r = printminiparentage(lp, ln, o->parent);
+	lp += r;
+	ln -= r;
+	if (ln <= 0) return 0;
+	if(o->value && o->type != File){
+		if(r && o->value && ln > 1){
+			*lp++ = '/';
+			ln--;
+		}
+		p = o->value;
+		while(ln > 0){
+			c = *p++;
+	    		if(c == '\n' || c == '\0')
+	    			break;
+			if(c == '~')
+				continue;
+			*lp++ = c;
+			ln--;
+		}
+	}
+	if(ln > 0)
+		*lp = '\0';
+	return sln - ln;
+}
+
+long
+printparentage(char *lp, int ln, Object *o) {
+	int i;
+	int r, k, sln;
+
+	if(ln <= 0)
+		return 0;
+	*lp = '\0';
+	if(o == 0 || o->type == Root)
+		return 0;
+	if(0)fprint(2, "parentage 0x%p %d type %d value 0x%p parent 0x%p %d\n", o, o->tabno, o->type, o->value, o->parent, o->parent->tabno);
+	if(o->orig){
+		if(0)fprint(2, "parentage 0x%p %d type %d orig %d type %d parent 0x%p %d\n", o, o->tabno, o->type, o->orig->tabno, o->orig->type, o->orig->parent, o->orig->parent->tabno);
+		o = o->orig;
+	}
+	sln = ln;
+	r = printparentage(lp, ln, o->parent);
+	lp += r; ln -= r;
+	if(o->type == File && fflag == 0){
+		if(ln > 0)
+			*lp = '\0';
+		return sln - ln;
+	}
+	if(o->value && *o->value && ln > 0){
+		if(o->type == Category){
+			if(o->parent == root){
+				r = snprint(lp, ln, "category: ");
+				lp += r; ln -= r;
+			}else{
+				for(k = Ntoken; k < ntoken; k++)
+					if(catseteq(&o->categories,&tokenlist[k].categories)){
+						r = snprint(lp, ln, "%s: ", tokenlist[k].name);
+						lp += r; ln -= r;
+						break;
+					}
+			}
+		}else{
+			r = snprint(lp, ln, "%s: ", tokenlist[o->type].name);
+			lp += r; ln -= r;
+		}
+		if(ln <= 0)
+			return sln - ln;
+		if(o->num){
+			r = snprint(lp, ln, "%2d. ", o->num);
+			lp += r; ln -= r;
+		}
+		if(ln <= 0)
+			return sln - ln;
+		r = indent(lp, ln, -1, o->value);
+		lp += r; ln -= r;
+		if(ln > 1){
+			*lp++ = '\n';
+			ln--;
+		}
+	}else{
+		if(0)fprint(2, "parentage 0x%p %d type %d no value\n", o, o->tabno, o->type);
+	}
+	for(i = 0; i < o->nchildren && ln > 0; i++)
+		switch(o->children[i]->type){
+		case Performance:
+		case Soloists:
+		case Lyrics:
+			r = snprint(lp, ln, "%s: ", tokenlist[o->children[i]->type].name);
+			lp += r; ln -= r;
+			if(ln <= 0)
+				break;
+			r = indent(lp, ln, -1, o->children[i]->value);
+			lp += r; ln -= r;
+			if(ln > 1){
+				*lp++ = '\n';
+				ln--;
+			}
+			break;
+		case Time:
+			r = snprint(lp, ln, "%s: %s\n", "duration", o->children[i]->value);
+			lp += r; ln -= r;
+			break;
+		case File:
+			if(fflag){
+				r = snprint(lp, ln, "%s: %s\n", "file", o->children[i]->value);
+				lp += r; ln -= r;
+			}
+			break;
+		default:
+			break;
+		}
+	if(ln > 0)
+		*lp = '\0';
+	return sln - ln;
+}
+
+long
+printparent(char *lp, int ln, Object *o) {
+	return snprint(lp, ln, "%d", o->parent->tabno);
+}
+
+long
+printkey(char *lp, int ln, Object *o) {
+	return snprint(lp, ln, "%s", o->key?o->key:o->value);
+}
+
+long
+printtype(char *lp, int ln, Object *o) {
+	return snprint(lp, ln, "%s", tokenlist[o->type].name);
+}
+
+long
+printtext(char *lp, int ln, Object *o) {
+	return snprint(lp, ln, "%s", o->value?o->value:o->key);
+}
+
+long
+printfulltext(char *lp, int ln, Object *o) {
+	char *sp, *p, *q;
+	int i, j, k, c, depth;
+	Object *oo;
+
+	depth = 0;
+	sp = lp;
+	switch(o->type){
+	case Category:
+		if(o->parent == root){
+			j = snprint(lp, ln, "category:");
+			lp += j; ln -= j;
+		}else{
+			for(k = Ntoken; k < ntoken; k++)
+				if(catseteq(&o->categories, &tokenlist[k].categories)){
+					j = snprint(lp, ln, "%s:", tokenlist[k].name);
+					lp += j; ln -= j;
+					break;
+				}
+		}
+		if(ln <= 0)
+			return lp - sp;
+		p = o->value;
+		if(p == nil)
+			p = o->key;
+		if((q = strchr(p, '\n')) && q[1] != '\0'){
+			// multiple lines
+			*lp++ = '\n'; ln--;
+			if(ln <= 0)
+				break;
+			j = indent(lp, ln, depth+1, p);
+			lp += j; ln -= j;
+		}else{
+			*lp++ = ' '; ln--;
+			while((c=*p++) && ln > 0){
+				if(c == '~')
+					continue;
+				*lp++ = c;
+				ln--;
+				if(c == '\n')
+					break;
+			}
+		}
+		break;
+	case Track:
+	case Part:
+	case Recording:
+	case Root:
+	case Search:
+	case Work:
+		j = snprint(lp, ln, "%s:", tokenlist[o->type].name);
+		lp += j; ln -= j;
+		if(ln <= 0)
+			break;
+		if(o->num){
+			j = snprint(lp, ln, " %2d.", o->num);
+			lp += j; ln -= j;
+		}
+		if(ln <= 0)
+			break;
+		p = o->value;
+		if(p == nil)
+			p = o->key;
+		if((q = strchr(p, '\n')) && q[1] != '\0'){
+			// multiple lines
+			*lp++ = '\n'; ln--;
+			if(ln <= 0)
+				break;
+			j = indent(lp, ln, depth+1, p);
+			lp += j; ln -= j;
+		}else{
+			*lp++ = ' '; ln--;
+			while((c =*p++) && ln > 0){
+				if(c == '~')
+					continue;
+				*lp++ = c;
+				ln--;
+				if(c == '\n')
+					break;
+			}
+		}
+	default:
+		break;
+	}
+	depth++;
+	for(i = 0; i < o->nchildren && ln > 0; i++){
+		oo = o->children[i];
+		switch(oo->type){
+		case Lyrics:
+		case Performance:
+		case Soloists:
+		case Time:
+			if (ln <= 4*depth + 1)
+				break;
+			*lp++ = '\n'; ln--;
+			memset(lp, ' ', 4*depth);
+			lp += 4*depth;
+			ln -= 4*depth;
+			if(ln <= 0)
+				break;
+			j = snprint(lp, ln, "%s:", tokenlist[oo->type].name);
+			lp += j; ln -= j;
+			if(ln <= 0)
+				break;
+			p = oo->value;
+			if(ln <= 1)
+				break;
+			if((q = strchr(p, '\n')) && q[1] != '\0'){
+				// multiple lines
+				*lp++ = '\n'; ln--;
+				j = indent(lp, ln, depth+1, p);
+				lp += j; ln -= j;
+			}else{
+				*lp++ = ' '; ln--;
+				while((c =*p++) && ln > 0){
+					if(c == '~')
+						continue;
+					*lp++ = c;
+					ln--;
+					if(c == '\n')
+						break;
+				}
+			}
+		}
+	}
+	*lp = '\0';
+	return lp - sp;
+}
+
+long
+printfiles(char *lp, int ln, Object *o) {
+	int i, r;
+	char *sp;
+
+	sp = lp;
+	if (o->type == File)
+		lp += snprint(lp, ln, "%d	%s\n", o->tabno, o->value);
+	else {
+		for (i = 0; i < o->nchildren && ln > 0; i++){
+			r = printfiles(lp, ln, o->children[i]);
+			lp += r;
+			ln -= r;
+		}
+	}
+	return lp - sp;
+}
+
+long
+printdigest(char *lp, int ln, Object *o) {
+	char *p;
+	int j, c, k;
+	char *sp;
+
+	sp = lp;
+	switch(o->type){
+	case Category:
+		if (o->parent == root) {
+			j = snprint(lp, ln, "category: ");
+			lp += j; ln -= j;
+		} else {
+			for (k = Ntoken; k < ntoken; k++)
+				if (catseteq(&o->categories,& tokenlist[k].categories)) {
+					j = snprint(lp, ln, "%s: ", tokenlist[k].name);
+					lp += j; ln -= j;
+//					break;
+				}
+		}
+		p = o->value;
+		if (p == 0) p = o->key;
+		while ((c=*p++) && c != '\n' && ln > 0) {
+			if(c == '~')
+				continue;
+			*lp++ = c;
+			ln--;
+		}
+		break;
+	case Track:
+	case Part:
+	case Recording:
+	case Root:
+	case Search:
+	case Work:
+		j = snprint(lp, ln, "%s: ", tokenlist[o->type].name);
+		lp += j; ln -= j;
+		if (o->num) {
+			j = snprint(lp, ln, "%2d. ", o->num);
+			lp += j; ln -= j;
+		}
+		p = o->value;
+		if (p == 0) p = o->key;
+		while ((c = *p++) && c != '\n' && ln > 0) {
+			if(c == '~')
+				continue;
+			*lp++ =  c;
+			ln--;
+		}
+	default:
+		break;
+	}
+	if(ln)
+		*lp = '\0';
+	return lp - sp;
+}
+
+#ifdef UNDEF
+
+void
+printtree(Object *o, int ind) {
+	char *p;
+	char buf[2048];
+	int i;
+
+	sprintf(buf, "%s {\n", tokenlist[o->type].name);
+	indent(ind, buf);
+	ind++;
+	if ((p = o->value)) {
+		sprintf(buf, "%s\n", p);
+		indent(ind, buf);
+	}
+	for (i = 0; i < o->nchildren; i++)
+		printtree(o->children[i], ind);
+	indent(--ind, "}\n");
+}
+
+void
+mapdump(Object *o, int depth, char *inheritance) {
+	Object *oo;
+	char *data;
+	int n, l;
+
+	if (o == root) {
+	    depth = 0;
+	    inheritance = "";
+	    data = NULL;
+	} else {
+	    if (o->value) {
+		l = nlines(o->value);
+	        n = strlen(inheritance) +
+		    l * (strlen(tokenlist[o->type].name) + 2) +
+		    strlen(o->value) + 2;
+		data = mymalloc(NULL, n);
+		strcpy(data, inheritance);
+		p = data + strlen(inheritance);
+		q = o->value;
+		while (*q) {
+		    p += sprintf(p, "%s:\t", tokenlist[o->type].name);
+		    do {
+			*p++ = *q;
+		    while (*q++ != '\n');
+		}
+		if (p[-1] != '\n') *p++ = '\n';
+		*p = 0;
+		inheritance = data;
+	    }
+	    indent(depth, inheritance);
+	}
+	c = 0;
+}
+
+#endif

+ 6 - 0
sys/src/games/music/jukefs/print.h

@@ -0,0 +1,6 @@
+extern int		fflag;
+
+void printtree(Object *, int);
+int parentage(char *, int, Object *);
+int miniparentage(char *, int, Object *);
+int indent(char *, int, int n, char *buf);

+ 44 - 0
sys/src/games/music/jukefs/search.c

@@ -0,0 +1,44 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "object.h"
+#include "parse.h"
+#include "search.h"
+
+Object *
+search(Object *rt, Object *parent, Reprog *preg) {
+	/* Create a `search object', a subtree of rt containing
+	 * only objects with s in their value of key fields plus
+	 * their parentage.
+	 *
+	 * Algorithm: depth-first traversal of rt.  On the way down,
+	 * copy rt to nr (new root), on the way back up, delete
+	 * subtrees without match.
+	 *
+	 * returns null when there are no matches in rt's subtree
+	 */
+	Object *o, *nr;
+	char *s;
+	int i;
+	int yes = 0;
+
+	nr = newobject(rt->type, parent);
+	nr->orig = rt->orig?rt->orig:rt;
+	nr->value = rt->value;
+	strncpy(nr->key, rt->key, KEYLEN);
+
+	if((((s = nr->value)) && regexec(preg, s, nil, 0) == 1)
+	|| (((s = nr->value)) && regexec(preg, s, nil, 0) == 1))
+		yes = 1;
+	for(i = 0; i < rt->nchildren; i++)
+		if((o = search((Object*)rt->children[i], nr, preg))){
+			yes = 1;
+			addchild(nr, o, "search");
+		}
+	if(yes == 0){
+		freeobject(nr, "s");
+		return nil;
+	}
+	return nr;
+}

+ 5 - 0
sys/src/games/music/jukefs/search.h

@@ -0,0 +1,5 @@
+#include <regexp.h>
+
+extern Object *sobj;
+
+Object *search(Object *rt, Object *parent, Reprog *preg);

+ 204 - 0
sys/src/games/music/jukefs/server.c

@@ -0,0 +1,204 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <bio.h>
+#include <fcall.h>
+#include "object.h"
+#include "parse.h"
+#include "print.h"
+#include "catset.h"
+#include "../debug.h"
+
+char		*user, *mapname, *svrname;
+int		p[2];
+int		mfd[2];
+int		debug = 0; //DBGSERVER|DBGSTATE|DBGPICKLE|DBGPLAY;
+Biobuf		*f;
+char		file[64];
+
+Object *root;
+
+Object **	otab;		// object table
+int		notab;	// no of entries used
+int		sotab;	// no of entries mallocated (invariant sotab >= notab)
+int		hotab;	// no of holes in otab;
+
+char usage[] = "Usage: %s [-f] [-l] [mapfile]\n";
+
+char *startdir;
+
+Object **catobjects;	/* for quickly finding category objects */
+int ncat = 0;
+
+void
+post(char *name, char *envname, int srvfd)
+{
+	int fd;
+	char buf[32];
+
+	fd = create(name, OWRITE, 0666);
+	if(fd < 0)
+		return;
+	sprint(buf, "%d",srvfd);
+	if(write(fd, buf, strlen(buf)) != strlen(buf))
+		sysfatal("srv write: %r");
+	close(fd);
+	putenv(envname, name);
+}
+
+int
+robusthandler(void*, char *s)
+{
+	if (debug) fprint(2, "inthandler: %s\n", s);
+	return (s && (strstr(s, "interrupted") || strstr(s, "hangup")));
+}
+
+long
+robustread(int fd, void *buf, long sz)
+{
+	long r;
+	char err[32];
+
+	do {
+		r = read(fd , buf, sz);
+		if (r < 0)
+			rerrstr(err, sizeof(err));
+	} while (r < 0 && robusthandler(nil, err));
+	return r;
+}
+
+void
+delobject(Object *o)
+{
+	/* Free an object and all its descendants */
+	Object *oo;
+	int i;
+
+	for (i = 0; i < o->nchildren; i++){
+		oo = o->children[i];
+		if (oo->parent == o)
+			delobject(oo);
+	}
+	freeobject(o, "r");
+}
+
+void
+threadmain(int argc, char *argv[]) {
+	char *q;
+	char *srvname;
+	char *mntpt;
+	int list;
+
+	mntpt = "/mnt";
+	user = strdup(getuser());
+	srvname = nil;
+	list = 0;
+
+	ARGBEGIN{
+	case 'l':
+		list = 1;
+		break;
+	case 'm':
+		mntpt = ARGF();
+		break;
+	case 'd':
+		debug = strtoul(ARGF(), nil, 0);
+		break;
+	case 's':
+		srvname = ARGF();
+		break;
+	case 'f':
+		fflag = 1;
+		break;
+	default:
+		fprint(2, usage, argv0);
+		exits("usage");
+	}ARGEND
+
+	switch (argc) {
+	default:
+		fprint(2, usage, argv0);
+		exits("usage");
+	case 0:
+		mapname = DEFAULTMAP;
+		break;
+	case 1:
+		mapname = argv[0];
+		break;
+	}
+
+	quotefmtinstall();
+
+	if((f = Bopen(mapname, OREAD)) == nil)
+		sysfatal("%s: %r", mapname);
+	strncpy(file, mapname, 256);
+	if ((q = strrchr(mapname, '/'))) *q = 0;
+	inittokenlist();
+	startdir = q?mapname:"";
+	getobject(Root, nil);
+	Bterm(f);
+	f = nil;
+	root->parent = root;
+
+	if(list){
+		listfiles(root);
+		threadexits(nil);
+	}
+
+	if(pipe(p) < 0)
+		sysfatal("pipe failed: %r");
+	mfd[0] = p[0];
+	mfd[1] = p[0];
+
+	threadnotify(robusthandler, 1);
+	user = strdup(getuser());
+
+	if(debug)
+		fmtinstall('F', fcallfmt);
+
+	procrfork(io, nil, 8192, RFFDG);	//RFNOTEG?
+
+	close(p[0]);	/* don't deadlock if child fails */
+
+	if(srvname){
+		srvname = smprint("/srv/jukefs.%s", srvname);
+		remove(srvname);
+		post(srvname, "jukefs", p[1]);
+	}
+	if(mount(p[1], -1, mntpt, MBEFORE, "") < 0)
+		sysfatal("mount failed: %r");
+	threadexits(nil);
+}
+
+void
+reread(void)
+{
+	int i;
+	extern int catnr;
+	char *q;
+
+	assert(f == nil);
+	if((f = Bopen(file, OREAD)) == nil)
+		fprint(2, "reread: %s: %r\n", file);
+	freetree(root);
+	root = nil;
+	for(i = 0; i< ntoken; i++){
+		free(tokenlist[i].name);
+		catsetfree(&tokenlist[i].categories);
+	}
+	catnr = 0;
+	free(tokenlist);
+	free(catobjects);
+	catobjects = nil;
+	ncat = 0;
+	tokenlist = nil;
+	ntoken = Ntoken;
+	inittokenlist();
+	strncpy(mapname, file, 256);
+	if ((q = strrchr(mapname, '/'))) *q = 0;
+	startdir = q?mapname:"";
+	getobject(Root, nil);
+	root->parent = root;
+	Bterm(f);
+	f = nil;
+}

+ 3 - 0
sys/src/games/music/missing

@@ -0,0 +1,3 @@
+#!/bin/rc
+
+for(i in `{games/jukefs -l | sort -u}) test -e $i || echo $i

+ 54 - 0
sys/src/games/music/mkfile

@@ -0,0 +1,54 @@
+dirs = playlistfs jukefs jukebox
+
+DEFAULTMAP = /sys/lib/music/map
+ICONPATH = /sys/lib/music/icon
+
+ICONS = \
+	next.bit\
+	pause.bit\
+	play.bit\
+	prev.bit\
+	question.bit\
+	root.bit\
+	skull.bit\
+	stop.bit\
+	trash.bit\
+
+ICONFILES = ${ICONS:%.bit=icon/%.bit}
+
+all:
+
+all dep clean:V:
+	for (i in $dirs) { 
+		echo $i
+		cd $i
+		mk $MKFLAGS $target
+		cd ..
+	}
+
+rcinstall:V:	juke.rc
+	cp juke.rc /rc/bin/juke
+	chmod +x /rc/bin/juke
+
+$ICONPATH:
+	mkdir $ICONPATH
+
+iconinstall:V:	$ICONFILES $ICONPATH
+	for (i in $ICONS) {
+		cp $ICONFILES $ICONPATH
+	}
+
+install:V:
+	for (i in $dirs) { 
+		echo $i
+		cd $i
+		mk $MKFLAGS $target
+		cd ..
+	}
+	mk rcinstall
+	mk iconinstall
+
+installall:V:
+	for(objtype in $CPUS)
+		mk $MKFLAGS install
+	mk rcinstall

+ 9 - 0
sys/src/games/music/mkinc

@@ -0,0 +1,9 @@
+base = ..
+CFLAGS=$CFLAGS $DEFINES
+
+all:
+
+dep:Q:
+	mkdep $INCLUDES $CFILES > mk.dep
+
+< mk.dep

+ 81 - 0
sys/src/games/music/playlistfs/boilerplate.c

@@ -0,0 +1,81 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <fcall.h>
+#include "playlist.h"
+
+static Channel *reqs;
+
+Req*
+reqalloc(void)
+{
+	Req *r;
+
+	if(reqs == nil)
+		reqs = chancreate(sizeof(Req*), 256);
+	if(r = nbrecvp(reqs))
+		return r;
+	r = malloc(sizeof(Req));
+	return r;
+}
+
+void
+reqfree(Req *r)
+{
+	if(!nbsendp(reqs, r))
+		free(r);
+}
+
+Wmsg
+waitmsg(Worker *w, Channel *q)
+{
+	Wmsg m;
+
+	sendp(q, w);
+	recv(w->eventc, &m);
+	return m;
+}
+
+int
+sendmsg(Channel *q, Wmsg *m)
+{
+	Worker *w;
+
+	if(w = nbrecvp(q))
+		send(w->eventc, m);
+	return w != nil;
+}
+
+void
+bcastmsg(Channel *q, Wmsg *m)
+{
+	Worker *w;
+	void *a;
+
+	a = m->arg;
+	while(w = nbrecvp(q)){
+		if(a) m->arg = strdup(a);
+		send(w->eventc, m);
+	}
+	free(a);
+	m->arg = nil;
+}
+
+void
+readbuf(Req *r, void *s, long n)
+{
+	r->ofcall.count = r->ifcall.count;
+	if(r->ifcall.offset >= n){
+		r->ofcall.count = 0;
+		return;
+	}
+	if(r->ifcall.offset+r->ofcall.count > n)
+		r->ofcall.count = n - r->ifcall.offset;
+	memmove(r->ofcall.data, (char*)s+r->ifcall.offset, r->ofcall.count);
+}
+
+void
+readstr(Req *r, char *s)
+{
+	readbuf(r, s, strlen(s));
+}

+ 901 - 0
sys/src/games/music/playlistfs/fs.c

@@ -0,0 +1,901 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <fcall.h>
+#include "pool.h"
+#include "playlist.h"
+
+typedef struct Wmsg Wmsg;
+
+enum {
+	Busy =	0x01,
+	Open =	0x02,
+	Trunc =	0x04,
+	Eof =	0x08,
+};
+
+File files[] = {
+[Qdir] =	{.dir = {0,0,{Qdir, 0,QTDIR},		0555|DMDIR,	0,0,0,	"."}},
+[Qplayctl] =	{.dir = {0,0,{Qplayctl, 0,QTFILE},	0666,		0,0,0,	"playctl"}},
+[Qplaylist] =	{.dir = {0,0,{Qplaylist, 0,QTFILE},	0666|DMAPPEND,	0,0,0,	"playlist"}},
+[Qplayvol] =	{.dir = {0,0,{Qplayvol, 0,QTFILE},	0666,		0,0,0,	"playvol"}},
+[Qplaystat] =	{.dir = {0,0,{Qplaystat, 0,QTFILE},	0444,		0,0,0,	"playstat"}},
+};
+
+Channel		*reqs;
+Channel		*workers;
+Channel		*volumechan;
+Channel		*playchan;
+Channel		*playlistreq;
+Playlist	playlist;
+int		volume[8];
+
+char *statetxt[] = {
+	[Nostate] =	"panic!",
+	[Error] =	"error",
+	[Stop] =	"stop",
+	[Pause] =	"pause",
+	[Play] =	"play",
+	[Resume] =	"resume",
+	[Skip] =	"skip",
+	nil
+};
+
+// low-order bits: position in play list, high-order: play state:
+Pmsg	playstate = {Stop, 0};
+
+char	*rflush(Worker*), *rauth(Worker*),
+	*rattach(Worker*), *rwalk(Worker*),
+	*ropen(Worker*), *rcreate(Worker*),
+	*rread(Worker*), *rwrite(Worker*), *rclunk(Worker*),
+	*rremove(Worker*), *rstat(Worker*), *rwstat(Worker*),
+	*rversion(Worker*);
+
+char 	*(*fcalls[])(Worker*) = {
+	[Tflush]	rflush,
+	[Tversion]	rversion,
+	[Tauth]		rauth,
+	[Tattach]	rattach,
+	[Twalk]		rwalk,
+	[Topen]		ropen,
+	[Tcreate]	rcreate,
+	[Tread]		rread,
+	[Twrite]	rwrite,
+	[Tclunk]	rclunk,
+	[Tremove]	rremove,
+	[Tstat]		rstat,
+	[Twstat]	rwstat,
+};
+
+int	messagesize = Messagesize;
+Fid	*fids;
+
+
+char	Eperm[] =	"permission denied";
+char	Enotdir[] =	"not a directory";
+char	Enoauth[] =	"authentication not required";
+char	Enotexist[] =	"file does not exist";
+char	Einuse[] =	"file in use";
+char	Eexist[] =	"file exists";
+char	Enotowner[] =	"not owner";
+char	Eisopen[] = 	"file already open for I/O";
+char	Excl[] = 	"exclusive use file already open";
+char	Ename[] = 	"illegal name";
+char	Ebadctl[] =	"unknown control message";
+char	Epast[] =	"reading past eof";
+
+Fid	*newfid(int);
+void	volumeupdater(void*);
+void	playupdater(void*);
+
+char *playerror;
+
+static int
+lookup(char *cmd, char *list[])
+{
+	int i;
+
+	for (i = 0; list[i] != nil; i++)
+		if (strcmp(cmd, list[i]) == 0)
+			return i;
+	return -1;
+}
+
+char*
+rversion(Worker *w)
+{
+	Req *r;
+	Fid *f;
+
+	r = w->r;
+	if(r->ifcall.msize < 256)
+		return "max messagesize too small";
+	if(r->ifcall.msize < messagesize)
+		messagesize = r->ifcall.msize;
+	r->ofcall.msize = messagesize;
+	if(strncmp(r->ifcall.version, "9P2000", 6) != 0)
+		return "unknown 9P version";
+	else
+		r->ofcall.version = "9P2000";
+	for(f = fids; f; f = f->next)
+		if(f->flags & Busy)
+			f->flags &= ~(Open|Busy);
+	return nil;
+}
+
+char*
+rauth(Worker*)
+{
+	return Enoauth;
+}
+
+char*
+rflush(Worker *w)
+{
+	Wmsg m;
+	int i;
+	Req *r;
+
+	r = w->r;
+	m.cmd = Flush;
+	m.off = r->ifcall.oldtag;
+	m.arg = nil;
+	for(i = 1; i < nelem(files); i++)
+		bcastmsg(files[i].workers, &m);
+	if (debug & DbgWorker)
+		fprint(2, "flush done on tag %d\n", r->ifcall.oldtag);
+	return 0;
+}
+
+char*
+rattach(Worker *w)
+{
+	Fid *f;
+	Req *r;
+
+	r = w->r;
+	f = r->fid;
+	f->flags |= Busy;
+	f->file = &files[Qdir];
+	r->ofcall.qid = f->file->dir.qid;
+	if(!aflag && strcmp(r->ifcall.uname, user) != 0)
+		return Eperm;
+	return 0;
+}
+
+static Fid*
+doclone(Fid *f, int nfid)
+{
+	Fid *nf;
+
+	nf = newfid(nfid);
+	if(nf->flags & Busy)
+		return nil;
+	nf->flags |= Busy;
+	nf->flags &= ~(Open);
+	nf->file = f->file;
+	return nf;
+}
+
+char*
+dowalk(Fid *f, char *name)
+{
+	int t;
+
+	if (strcmp(name, ".") == 0)
+		return nil;
+	if (strcmp(name, "..") == 0){
+		f->file = &files[Qdir];
+		return nil;
+	}
+	if(f->file != &files[Qdir])
+		return Enotexist;
+	for (t = 1; t < Nqid; t++){
+		if(strcmp(name, files[t].dir.name) == 0){
+			f->file = &files[t];
+			return nil;
+		}
+	}
+	return Enotexist;
+}
+
+char*
+rwalk(Worker *w)
+{
+	Fid *f, *nf;
+	char *rv;
+	int i;
+	File *savefile;
+	Req *r;
+
+	r = w->r;
+	f = r->fid;
+	if(f->flags & Open)
+		return Eisopen;
+
+	r->ofcall.nwqid = 0;
+	nf = nil;
+	savefile = f->file;
+	/* clone if requested */
+	if(r->ifcall.newfid != r->ifcall.fid){
+		nf = doclone(f, r->ifcall.newfid);
+		if(nf == nil)
+			return "new fid in use";
+		f = nf;
+	}
+
+	/* if it's just a clone, return */
+	if(r->ifcall.nwname == 0 && nf != nil)
+		return nil;
+
+	/* walk each element */
+	rv = nil;
+	for(i = 0; i < r->ifcall.nwname; i++){
+		rv = dowalk(f, r->ifcall.wname[i]);
+		if(rv != nil){
+			if(nf != nil)	
+				nf->flags &= ~(Open|Busy);
+			else
+				f->file = savefile;
+			break;
+		}
+		r->ofcall.wqid[i] = f->file->dir.qid;
+	}
+	r->ofcall.nwqid = i;
+
+	/* we only error out if no walk  */
+	if(i > 0)
+		rv = nil;
+
+	return rv;
+}
+
+char *
+ropen(Worker *w)
+{
+	Fid *f, *ff;
+	Wmsg m;
+	Req *r;
+
+	r = w->r;
+	f = r->fid;
+	if(f->flags & Open)
+		return Eisopen;
+
+	if(r->ifcall.mode != OREAD)
+		if((f->file->dir.mode & 0x2) == 0)
+			return Eperm;
+	if((r->ifcall.mode & OTRUNC) && f->file == &files[Qplaylist]){
+		playlist.nlines = 0;
+		playlist.ndata = 0;
+		free(playlist.lines);
+		free(playlist.data);
+		playlist.lines = nil;
+		playlist.data = nil;
+		f->file->dir.length = 0;
+		f->file->dir.qid.vers++;
+		/* Mark all fids for this file `Trunc'ed */
+		for(ff = fids; ff; ff = ff->next)
+			if(ff->file == &files[Qplaylist] && (ff->flags & Open))
+				ff->flags |= Trunc;
+		m.cmd = Check;
+		m.off = 0;
+		m.arg = nil;
+		bcastmsg(f->file->workers, &m);
+	}
+	r->ofcall.iounit = 0;
+	r->ofcall.qid = f->file->dir.qid;
+	f->flags |= Open;
+	return nil;
+}
+
+char *
+rcreate(Worker*)
+{
+	return Eperm;
+}
+
+int
+readtopdir(Fid*, uchar *buf, long off, int cnt, int blen)
+{
+	int i, m, n;
+	long pos;
+
+	n = 0;
+	pos = 0;
+	for (i = 1; i < Nqid; i++){
+		m = convD2M(&files[i].dir, &buf[n], blen-n);
+		if(off <= pos){
+			if(m <= BIT16SZ || m > cnt)
+				break;
+			n += m;
+			cnt -= m;
+		}
+		pos += m;
+	}
+	return n;
+}
+
+char*
+rread(Worker *w)
+{
+	Fid *f;
+	Req *r;
+	long off, cnt;
+	int n, i;
+	Wmsg m;
+	char *p;
+
+	r = w->r;
+	f = r->fid;
+	r->ofcall.count = 0;
+	off = r->ifcall.offset;
+	cnt = r->ifcall.count;
+
+	if(cnt > messagesize - IOHDRSZ)
+		cnt = messagesize - IOHDRSZ;
+
+	if(f->file == &files[Qdir]){
+		n = readtopdir(f, r->indata, off, cnt, messagesize - IOHDRSZ);
+		r->ofcall.count = n;
+		return nil;
+	}
+
+	if(f->file == files + Qplaystat){
+		p = getplaystat(r->ofcall.data, r->ofcall.data + sizeof r->indata);
+		readbuf(r, r->ofcall.data, p - r->ofcall.data);
+		return nil;
+	}
+
+	m.cmd = 0;
+	while(f->vers == f->file->dir.qid.vers && (f->flags & Eof)){
+		/* Wait until file state changes (f->file->dir.qid.vers is incremented) */
+		m = waitmsg(w, f->file->workers);
+		if(m.cmd == Flush && m.off == r->ifcall.tag)
+			return (char*)~0;	/* no answer needed */
+		assert(m.cmd != Work);
+		yield();
+	}
+	if(f->file == files + Qplaylist){
+		f->flags &= ~Eof;
+		if((f->flags & Trunc) && r->ifcall.offset != 0){
+			f->flags &= ~Trunc;
+			return Epast;
+		}
+		f->flags &= ~Trunc;
+		if(r->ifcall.offset < playlist.ndata)
+			readbuf(r, playlist.data, playlist.ndata);
+		else if(r->ifcall.offset == playlist.ndata){
+			r->ofcall.count = 0;
+			/* Arrange for this fid to wait next time: */
+			f->vers = f->file->dir.qid.vers;
+			f->flags |= Eof;
+		}else{
+			/* Beyond eof, bad seek? */
+			return Epast;
+		}
+	}else if(f->file == files + Qplayctl){
+		f->flags &= ~Eof;
+		if(m.cmd == Error){
+			snprint(r->ofcall.data, sizeof r->indata, "%s %ud %q",
+				statetxt[m.cmd], m.off, m.arg);
+			free(m.arg);
+		}else if(f->vers == f->file->dir.qid.vers){
+			r->ofcall.count = 0;
+			/* Arrange for this fid to wait next time: */
+			f->flags |= Eof;
+			return nil;
+		}else{
+			snprint(r->ofcall.data, sizeof r->indata, "%s %ud",
+				statetxt[playstate.cmd], playstate.off);
+			f->vers = f->file->dir.qid.vers;
+		}
+		r->ofcall.count = strlen(r->ofcall.data);
+		if(r->ofcall.count > r->ifcall.count)
+			r->ofcall.count = r->ifcall.count;
+	}else if(f->file == files + Qplayvol){
+		f->flags &= ~Eof;
+		if(f->vers == f->file->dir.qid.vers){
+			r->ofcall.count = 0;
+			/* Arrange for this fid to wait next time: */
+			f->flags |= Eof;
+		}else{
+			p = seprint(r->ofcall.data, r->ofcall.data + sizeof r->indata, "volume	'");
+			for(i = 0; i < nelem(volume); i++){
+				if(volume[i] == Undef)
+					break;
+				p = seprint(p, r->ofcall.data + sizeof r->indata, "%d ", volume[i]);
+			}
+			p = seprint(p, r->ofcall.data + sizeof r->indata, "'");
+			r->ofcall.count = p - r->ofcall.data;
+			if(r->ofcall.count > r->ifcall.count)
+				r->ofcall.count = r->ifcall.count;
+			f->vers = f->file->dir.qid.vers;
+		}
+	}else
+		abort();
+	return nil;
+}
+
+static char*
+qtoken(char *s, char *sep)
+{
+	int quoting;
+	char *t;
+
+	quoting = 0;
+	t = s;	/* s is output string, t is input string */
+	while(*t!='\0' && (quoting || utfrune(sep, *t)==nil)){
+		if(*t != '\''){
+			*s++ = *t++;
+			continue;
+		}
+		/* *t is a quote */
+		if(!quoting){
+			quoting = 1;
+			t++;
+			continue;
+		}
+		/* quoting and we're on a quote */
+		if(t[1] != '\''){
+			/* end of quoted section; absorb closing quote */
+			t++;
+			quoting = 0;
+			continue;
+		}
+		/* doubled quote; fold one quote into two */
+		t++;
+		*s++ = *t++;
+	}
+	if(*t == '\0')
+		return nil;
+	return t;
+}
+
+char*
+rwrite(Worker *w)
+{
+	long cnt, i, nf, cmd;
+	Pmsg newstate;
+	char *fields[3], *p, *q;
+	Wmsg m;
+	Fid *f;
+	Req *r;
+
+	r = w->r;
+	f = r->fid;
+	r->ofcall.count = 0;
+	cnt = r->ifcall.count;
+
+	if(cnt > messagesize - IOHDRSZ)
+		cnt = messagesize - IOHDRSZ;
+
+	if(f->file == &files[Qplayctl]){
+		r->ifcall.data[cnt] = '\0';
+		if (debug & DbgPlayer)
+			fprint(2, "rwrite playctl: %s\n", r->ifcall.data);
+		nf = tokenize(r->ifcall.data, fields, 4);
+		if (nf == 0){
+			r->ofcall.count = r->ifcall.count;
+			return nil;
+		}
+		if (nf == 2)
+			i = strtol(fields[1], nil, 0);
+		else
+			i = playstate.off;
+		newstate = playstate;
+		if ((cmd = lookup(fields[0], statetxt)) < 0)
+			return  Ebadctl;
+		switch(cmd){
+		case Play:
+			newstate.cmd = cmd;
+			newstate.off = i;
+			break;
+		case Pause:
+			if (playstate.cmd != Play)
+				break;
+			// fall through
+		case Stop:
+			newstate.cmd = cmd;
+			newstate.off = playstate.off;
+			break;
+		case Resume:
+			if(playstate.cmd == Stop)
+				break;
+			newstate.cmd = Resume;
+			newstate.off = playstate.off;
+			break;
+		case Skip:
+			if (nf == 2)
+				i += playstate.off;
+			else
+				i = playstate.off +1;
+			if(i < 0)
+				i = 0;
+			else if (i >= playlist.nlines)
+				i = playlist.nlines - 1;
+			newstate.cmd = Play;
+			newstate.off = i;
+		}
+		if (newstate.off >= playlist.nlines){
+			newstate.cmd = Stop;
+			newstate.off = playlist.nlines;
+		}
+		if (debug & DbgPlayer)
+			fprint(2, "new state %s-%ud\n",
+				statetxt[newstate.cmd], newstate.off);
+		if (newstate.m != playstate.m)
+			sendul(playc, newstate.m);
+		f->file->dir.qid.vers++;
+	} else if(f->file == &files[Qplayvol]){
+		char *subfields[nelem(volume)];
+		int v[nelem(volume)];
+
+		r->ifcall.data[cnt] = '\0';
+		if (debug & DbgPlayer)
+			fprint(2, "rwrite playvol: %s\n", r->ifcall.data);
+		nf = tokenize(r->ifcall.data, fields, 4);
+		if (nf == 0){
+			r->ofcall.count = r->ifcall.count;
+			return nil;
+		}
+		if (nf != 2 || strcmp(fields[0], "volume") != 0)
+			return Ebadctl;
+		if (debug & DbgPlayer)
+			fprint(2, "new volume '");
+		nf = tokenize(fields[1], subfields, nelem(subfields));
+		if (nf <= 0 || nf > nelem(volume))
+			return "volume";
+		for (i = 0; i < nf; i++){
+			v[i] = strtol(subfields[i], nil, 0);
+			if (debug & DbgPlayer)
+				fprint(2, " %d", v[i]);
+		}
+		if (debug & DbgPlayer)
+			fprint(2, "'\n");
+		while (i < nelem(volume))
+			v[i++] = Undef;
+		volumeset(v);
+		r->ofcall.count = r->ifcall.count;
+		return nil;
+	} else if(f->file == &files[Qplaylist]){
+		if (debug & DbgPlayer){
+			fprint(2, "rwrite playlist: `");
+			write(2, r->ifcall.data, cnt);
+			fprint(2, "'\n");
+		}
+		playlist.data = realloc(playlist.data, playlist.ndata + cnt + 2);
+		if (playlist.data == 0)
+			sysfatal("realloc: %r");
+		memmove(playlist.data + playlist.ndata, r->ifcall.data, cnt);
+		if (playlist.data[playlist.ndata + cnt-1] != '\n')
+			playlist.data[playlist.ndata + cnt++] = '\n';
+		playlist.data[playlist.ndata + cnt] = '\0';
+		p = playlist.data + playlist.ndata;
+		while (*p){
+			playlist.lines = realloc(playlist.lines, (playlist.nlines+1)*sizeof(playlist.lines[0]));
+			if(playlist.lines == nil)
+				sysfatal("realloc: %r");
+			playlist.lines[playlist.nlines] = playlist.ndata;
+			q = strchr(p, '\n');
+			if (q == nil)
+				break;
+			if(debug & DbgPlayer)
+				fprint(2, "[%lud]: ", playlist.nlines);
+			playlist.nlines++;
+			q++;
+			if(debug & DbgPlayer)
+				write(2, p, q-p);
+			playlist.ndata += q - p;
+			p = q;
+		}
+		f->file->dir.length = playlist.ndata;
+		f->file->dir.qid.vers++;
+	}else
+		return Eperm;
+	r->ofcall.count = r->ifcall.count;
+	m.cmd = Check;
+	m.off = 0;
+	m.arg = nil;
+	bcastmsg(f->file->workers, &m);
+	return nil;
+}
+
+char *
+rclunk(Worker *w)
+{
+	w->r->fid->flags &= ~(Open|Busy);
+	return 0;
+}
+
+char *
+rremove(Worker*)
+{
+	return Eperm;
+}
+
+char *
+rstat(Worker *w)
+{
+	Req *r;
+
+	r = w->r;
+	r->ofcall.nstat = convD2M(&r->fid->file->dir, r->indata, messagesize - IOHDRSZ);
+	r->ofcall.stat = r->indata;
+	return 0;
+}
+
+char *
+rwstat(Worker*)
+{
+	return Eperm;
+}
+
+Fid *
+newfid(int fid)
+{
+	Fid *f, *ff;
+
+	ff = nil;
+	for(f = fids; f; f = f->next)
+		if(f->fid == fid){
+			return f;
+		}else if(ff == nil && (f->flags & Busy) == 0)
+			ff = f;
+	if(ff == nil){
+		ff = malloc(sizeof *ff);
+		if (ff == nil)
+			sysfatal("malloc: %r");
+		memset(ff, 0, sizeof *ff);
+		ff->next = fids;
+		ff->readers = 0;
+		fids = ff;
+	}
+	ff->fid = fid;
+	ff->file = nil;
+	ff->vers = ~0;
+	return ff;
+}
+
+void
+work(Worker *w)
+{
+	Req *r;
+	char *err;
+	int n;
+
+	r = w->r;
+	r->ofcall.data = (char*)r->indata;
+	if(!fcalls[r->ifcall.type])
+		err = "bad fcall type";
+	else {
+		r->fid = newfid(r->ifcall.fid);
+		err = (*fcalls[r->ifcall.type])(w);
+	}
+	if(err != (char*)~0){	/* ~0 indicates Flush received */
+		if(err){
+			r->ofcall.type = Rerror;
+			r->ofcall.ename = err;
+		}else{
+			r->ofcall.type = r->ifcall.type + 1;
+			r->ofcall.fid = r->ifcall.fid;
+		}
+		r->ofcall.tag = r->ifcall.tag;
+		if(debug & DbgFs)
+			fprint(2, "io:->%F\n", &r->ofcall);/**/
+		n = convS2M(&r->ofcall, r->outdata, messagesize);
+		if(write(srvfd[0], r->outdata, n) != n)
+			sysfatal("mount write");
+	}
+	reqfree(r);
+	w->r = nil;
+}
+
+void
+worker(void *arg)
+{
+	Worker *w;
+	Wmsg m;
+
+	w = arg;
+	recv(w->eventc, &m);
+	for(;;){
+		assert(m.cmd == Work);
+		w->r = m.arg;
+		if(debug & DbgWorker)
+			fprint(2, "worker 0x%p:<-%F\n", w, &w->r->ifcall);
+		work(w);
+		if(debug & DbgWorker)
+			fprint(2, "worker 0x%p wait for next\n", w);
+		m = waitmsg(w, workers);
+	}
+}
+
+void
+allocwork(Req *r)
+{
+	Worker *w;
+	Wmsg m;
+
+	m.cmd = Work;
+	m.off = 0;
+	m.arg = r;
+	if(sendmsg(workers, &m))
+		return;
+	/* No worker ready to accept request, allocate one */
+	w = malloc(sizeof(Worker));
+	w->eventc = chancreate(sizeof(Wmsg), 1);
+	if(debug & DbgWorker)
+		fprint(2, "new worker 0x%p\n", w);/**/
+	threadcreate(worker, w, 4096);
+	send(w->eventc, &m);
+}
+
+void
+srvio(void *arg)
+{
+	char e[32];
+	int n;
+	Req *r;
+	Channel *dispatchc;
+
+	threadsetname("file server IO");
+	dispatchc = arg;
+
+	r = reqalloc();
+	for(;;){
+		/*
+		 * reading from a pipe or a network device
+		 * will give an error after a few eof reads
+		 * however, we cannot tell the difference
+		 * between a zero-length read and an interrupt
+		 * on the processes writing to us,
+		 * so we wait for the error
+		 */
+		n = read9pmsg(srvfd[0], r->indata, messagesize);
+		if(n == 0)
+			continue;
+		if(n < 0){
+			rerrstr(e, sizeof e);
+			if (strcmp(e, "interrupted") == 0){
+				if (debug & DbgFs) fprint(2, "read9pmsg interrupted\n");
+				continue;
+			}
+			sysfatal("srvio: %s", e);
+		}
+		if(convM2S(r->indata, n, &r->ifcall) == 0)
+			continue;
+
+		if(debug & DbgFs)
+			fprint(2, "io:<-%F\n", &r->ifcall);
+		sendp(dispatchc, r);
+		r = reqalloc();
+	}
+}
+
+char *
+getplaylist(int n)
+{
+	Wmsg m;
+
+	m.cmd = Preq;
+	m.off = n;
+	m.arg = nil;
+	send(playlistreq, &m);
+	recv(playlistreq, &m);
+	if(m.cmd == Error)
+		return nil;
+	assert(m.cmd == Prep);
+	assert(m.arg);
+	return m.arg;
+}
+
+void
+playlistsrv(void*)
+{
+	Wmsg m;
+	char *p, *q, *r;
+	char *fields[2];
+	int n;
+	/* Runs in the srv proc */
+
+	threadsetname("playlistsrv");
+	while(recv(playlistreq, &m)){
+		assert(m.cmd == Preq);
+		m.cmd = Error;
+		if(m.off < playlist.nlines){
+			p = playlist.data + playlist.lines[m.off];
+			q = strchr(p, '\n');
+			if (q == nil)
+				sysfatal("playlistsrv: no newline character found");
+			n = q-p;
+			r = malloc(n+1);
+			memmove(r, p, n);
+			r[n] = 0;
+			tokenize(r, fields, nelem(fields));
+			assert(fields[0] == r);
+			m.cmd = Prep;
+			m.arg = r;
+		}
+		send(playlistreq, &m);
+	}
+}
+
+void
+srv(void*)
+{
+	Req *r;
+	Channel *dispatchc;
+	/*
+	 * This is the proc with all the action.
+	 * When a file request comes in, it is dispatched to this proc
+	 * for processing.  Two extra threads field changes in play state
+	 * and volume state.
+	 * By keeping all the action in this proc, we won't need locks
+	 */
+
+	threadsetname("srv");
+	close(srvfd[1]);
+
+	dispatchc = chancreate(sizeof(Req*), 1);
+	procrfork(srvio, dispatchc, 4096, RFFDG);
+
+	threadcreate(volumeupdater, nil, 4096);
+	threadcreate(playupdater, nil, 4096);
+	threadcreate(playlistsrv, nil, 4096);
+
+	while(r = recvp(dispatchc))
+		allocwork(r);
+		
+}
+
+void
+playupdater(void*)
+{
+	Wmsg m;
+	/* This is a thread in the srv proc */
+
+	while(recv(playchan, &m)){
+		if(debug & DbgPlayer)
+			fprint(2, "playupdate: %s %d %s\n", statetxt[m.cmd], m.off, m.arg?m.arg:"");
+		if(playstate.m == m.m)
+			continue;
+		if(m.cmd == Stop && m.off == 0xffff)
+			m.off = playlist.nlines;
+		if(m.cmd != Error){
+			playstate.m = m.m;
+			m.cmd = Check;
+			assert(m.arg == nil);
+		}
+		files[Qplayctl].dir.qid.vers++;
+		bcastmsg(files[Qplayctl].workers, &m);
+	}
+}
+
+void
+volumeupdater(void*)
+{
+	Wmsg m;
+	int v[nelem(volume)];
+	/* This is a thread in the srv proc */
+
+	while(recv(volumechan, v)){
+		if(debug & DbgPlayer)
+			fprint(2, "volumeupdate: volume now %d %d %d %d\n", volume[0], volume[1], volume[2], volume[3]);
+		memmove(volume, v, sizeof(volume));
+		files[Qplayvol].dir.qid.vers++;
+		m.cmd = Check;
+		m.arg = nil;
+		bcastmsg(files[Qplayvol].workers, &m);
+	}
+}
+
+void
+playupdate(Pmsg p, char *s)
+{
+	Wmsg m;
+
+	m.m = p.m;
+	m.arg = s ? strdup(s) : nil;
+	send(playchan, &m);
+}

+ 94 - 0
sys/src/games/music/playlistfs/main.c

@@ -0,0 +1,94 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <fcall.h>
+#include "playlist.h"
+
+int	debug;
+char	*user;
+int	srvfd[2];
+int	aflag;
+
+void
+usage(void)
+{
+	sysfatal("usage: %s [-d bitmask] [-s] [-m mountpoint]", argv0);
+}
+
+void
+post(char *name, int sfd)
+{
+	int fd;
+	char buf[32];
+
+	fd = create(name, OWRITE, 0666);
+	if(fd < 0)
+		return;
+	sprint(buf, "%d", sfd);
+	if(write(fd, buf, strlen(buf)) != strlen(buf))
+		sysfatal("srv write: %r");
+	close(fd);
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	char *srvfile;
+	char *srvpost;
+	char *mntpt;
+	int i;
+
+	mntpt = "/mnt";
+	srvpost = nil;
+
+	rfork(RFNOTEG);
+
+	ARGBEGIN{
+	case 'a':
+		aflag = 1;
+		break;
+	case 'm':
+		mntpt = ARGF();
+		break;
+	case 'd':
+		debug = strtoul(ARGF(), nil, 0);
+		break;
+	case 's':
+		srvpost = ARGF();
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	user = strdup(getuser());
+
+	quotefmtinstall();
+
+	if(debug)
+		fmtinstall('F', fcallfmt);
+
+	volumechan = chancreate(sizeof(volume), 1);
+	playchan = chancreate(sizeof(Wmsg), 1);
+	playlistreq = chancreate(sizeof(Wmsg), 0);	/* No storage! requires rendez-vous */
+	workers = chancreate(sizeof(Worker*), 256);
+	for(i = 1; i < Nqid; i++)
+		files[i].workers = chancreate(sizeof(Worker*), 256);
+
+	if(pipe(srvfd) < 0)
+		sysfatal("pipe failed: %r");
+	procrfork(srv, nil, 8192, RFFDG);
+	close(srvfd[0]);	/* don't deadlock if child fails */
+
+	procrfork(volumeproc, nil, 8192, RFFDG);
+	playinit();
+
+	if(srvpost){
+		srvfile = smprint("/srv/playlist.%s", srvpost);
+		remove(srvfile);
+		post(srvfile, srvfd[1]);
+		free(srvfile);
+	}
+	if(mount(srvfd[1], -1, mntpt, MBEFORE, "") < 0)
+		sysfatal("mount failed: %r");
+	threadexits(nil);
+}

+ 5 - 0
sys/src/games/music/playlistfs/mk.dep

@@ -0,0 +1,5 @@
+main.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/fcall.h playlist.h
+fs.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/fcall.h /sys/include/pool.h playlist.h
+player.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/fcall.h /sys/include/pool.h playlist.h
+volume.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/fcall.h /sys/include/pool.h playlist.h
+boilerplate.$O: /$objtype/include/u.h /sys/include/libc.h /sys/include/thread.h /sys/include/fcall.h playlist.h

+ 16 - 0
sys/src/games/music/playlistfs/mkfile

@@ -0,0 +1,16 @@
+</$objtype/mkfile
+<../mkinc
+
+TARG = playlistfs
+BIN = /$objtype/bin/games
+
+CFILES=\
+	main.c\
+	fs.c\
+	player.c\
+	volume.c\
+	boilerplate.c\
+
+OFILES = ${CFILES:%.c=%.$O}
+
+</sys/src/cmd/mkone

+ 413 - 0
sys/src/games/music/playlistfs/player.c

@@ -0,0 +1,413 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <fcall.h>
+#include "pool.h"
+#include "playlist.h"
+
+enum {
+	Pac,
+	Mp3,
+	Pcm,
+	Ogg,
+};
+
+typedef struct Playfd Playfd;
+
+struct Playfd {
+	/* Describes a file to play for starting up pac4dec/mp3,... */
+	char	*filename;	/* mallocated */
+	int	fd;		/* filedesc to use */
+	int	cfd;		/* fildesc to close */
+};
+
+Channel *full, *empty, *playout, *spare;
+Channel	*playc, *pacc;
+
+char *playprog[] = {
+[Pac] = "/bin/games/pac4dec",
+[Mp3] = "/bin/games/mp3dec",
+[Pcm] = "/bin/cp",
+[Ogg] = "/bin/games/vorbisdec",
+};
+
+ulong totbytes, totbuffers;
+
+static char curfile[8192];
+
+void
+pac4dec(void *a)
+{
+	Playfd *pfd;
+	Pacbuf *pb;
+	int fd, type;
+	char *ext, buf[256];
+	static char args[6][32];
+	char *argv[6] = {args[0], args[1], args[2], args[3], args[4], args[5]};
+
+	threadsetname("pac4dec");
+	pfd = a;
+	close(pfd->cfd);	/* read fd */
+	ext = strrchr(pfd->filename, '.');
+	fd = open(pfd->filename, OREAD);
+	if (fd < 0 && ext == nil){
+		// Try the alternatives
+		ext = buf + strlen(pfd->filename);
+		snprint(buf, sizeof buf, "%s.pac", pfd->filename);
+		fd = open(buf, OREAD);
+		if (fd < 0){
+			snprint(buf, sizeof buf, "%s.mp3", pfd->filename);
+			fd = open(buf, OREAD);
+		}
+		if (fd < 0){
+			snprint(buf, sizeof buf, "%s.ogg", pfd->filename);
+			fd = open(buf, OREAD);
+		}
+		if (fd < 0){
+			snprint(buf, sizeof buf, "%s.pcm", pfd->filename);
+			fd = open(buf, OREAD);
+		}
+	}
+	if (fd < 0){
+		if (debug & DbgPlayer)
+			fprint(2, "pac4dec: %s: %r", pfd->filename);
+		pb = nbrecvp(spare);
+		pb->cmd = Error;
+		pb->off = 0;
+		pb->len = snprint(pb->data, sizeof(pb->data), "startplay: %s: %r", pfd->filename);
+		sendp(full, pb);
+		threadexits("open");
+	}
+	dup(pfd->fd, 1);
+	close(pfd->fd);
+	if(ext == nil || strcmp(ext, ".pac") == 0){
+		type = Pac;
+		snprint(args[0], sizeof args[0], "pac4dec");
+		snprint(args[1], sizeof args[1], "/fd/%d", fd);
+		snprint(args[2], sizeof args[2], "/fd/1");
+		argv[3] = nil;
+	}else if(strcmp(ext, ".mp3") == 0){
+		type = Mp3;
+		snprint(args[0], sizeof args[0], "mp3dec");
+		snprint(args[1], sizeof args[1], "-q");
+		snprint(args[2], sizeof args[1], "-s");
+		snprint(args[3], sizeof args[1], "/fd/%d", fd);
+		argv[4] = nil;
+	}else if(strcmp(ext, ".ogg") == 0){
+		type = Ogg;
+		snprint(args[0], sizeof args[0], "vorbisdec");
+		argv[1] = nil;
+		argv[2] = nil;
+		argv[3] = nil;
+		dup(fd, 0);
+	}else{
+		type = Pcm;
+		snprint(args[0], sizeof args[0], "cat");
+		snprint(args[1], sizeof args[1], "/fd/%d", fd);
+		argv[2] = nil;
+		argv[3] = nil;
+	}
+	free(pfd->filename);
+	free(pfd);
+	if (debug & DbgPlayer)
+		fprint(2, "procexecl %s %s %s %s\n",
+			playprog[type], argv[0], argv[1], argv[2]);
+	procexec(nil, playprog[type], argv);
+	if((pb = nbrecvp(spare)) == nil)
+		pb = malloc(sizeof(Pacbuf));
+	pb->cmd = Error;
+	pb->off = 0;
+	pb->len = snprint(pb->data, sizeof(pb->data), "startplay: %s: exec", playprog[type]);
+	sendp(full, pb);
+	threadexits(playprog[type]);
+}
+
+static int
+startplay(ushort n)
+{
+	int fd[2];
+	Playfd *pfd;
+	char *file;
+
+	file = getplaylist(n);
+	if(file == nil)
+		return Undef;
+	if (debug & DbgPlayer)
+		fprint(2, "startplay: file is `%s'\n", file);
+	if(pipe(fd) < 0)
+		sysfatal("pipe: %r");
+	pfd = malloc(sizeof(Playfd));
+	pfd->filename = file;	/* mallocated already */
+	pfd->fd = fd[1];
+	pfd->cfd = fd[0];
+	procrfork(pac4dec, pfd, 4096, RFFDG);
+	close(fd[1]);	/* write fd, for pac4dec */
+	return fd[0];	/* read fd */
+}
+
+static void
+rtsched(void)
+{
+	int fd;
+	char *ctl;
+
+	ctl = smprint("/proc/%ud/ctl", getpid());
+	if((fd = open(ctl, ORDWR)) < 0) 
+		sysfatal("%s: %r", ctl);
+	if(fprint(fd, "period 20ms") < 0)
+		sysfatal("%s: %r", ctl);
+	if(fprint(fd, "cost 100µs") < 0)
+		sysfatal("%s: %r", ctl);
+	if(fprint(fd, "sporadic") < 0)
+		sysfatal("%s: %r", ctl);
+	if(fprint(fd, "admit") < 0)
+		sysfatal("%s: %r", ctl);
+	close(fd);
+	free(ctl);
+}
+
+void
+pacproc(void*)
+{
+	Pmsg playstate, newstate;
+	int fd;
+	Pacbuf *pb;
+	Alt a[3] = {
+		{empty, &pb, CHANNOP},
+		{playc, &newstate.m, CHANRCV},
+		{nil, nil, CHANEND},
+	};
+
+	threadsetname("pacproc");
+	close(srvfd[1]);
+	newstate.cmd = playstate.cmd = Stop;
+	newstate.off = playstate.off = 0;
+	fd = -1;
+	for(;;){
+		switch(alt(a)){
+		case 0:
+			/* Play out next buffer (pb points to one already) */
+			assert(fd >= 0);	/* Because we must be in Play mode */
+			pb->m = playstate.m;
+			pb->len = read(fd, pb->data, sizeof pb->data);
+			if(pb->len > 0){
+				sendp(full, pb);
+				break;
+			}
+			if(pb->len < 0){
+				if(debug & DbgPlayer)
+					fprint(2, "pac, error: %d\n", playstate.off);
+				pb->cmd = Error;
+				pb->len = snprint(pb->data, sizeof pb->data, "%s: %r", curfile);
+				sendp(full, pb);
+			}else{
+				/* Simple end of file */
+				sendp(empty, pb); /* Don't need buffer after all */
+			}
+			close(fd);
+			fd = -1;
+			if(debug & DbgPlayer)
+				fprint(2, "pac, eof: %d\n", playstate.off);
+			/* End of file, do next by falling through */
+			newstate.cmd = playstate.cmd;
+			newstate.off = playstate.off + 1;
+		case 1:
+			if((debug & DbgPac) && newstate.cmd)
+				fprint(2, "Pacproc: newstate %s-%d, playstate %s-%d\n",
+					statetxt[newstate.cmd], newstate.off,
+					statetxt[playstate.cmd], playstate.off);
+			/* Deal with an incoming command */
+			if(newstate.cmd == Pause || newstate.cmd == Resume){
+				/* Just pass them on, don't change local state */
+				pb = recvp(spare);
+				pb->m = newstate.m;
+				sendp(full, pb);
+				break;
+			}
+			/* Stop whatever we're doing */
+			if(fd >= 0){
+				if(debug & DbgPlayer)
+					fprint(2, "pac, stop\n");
+				/* Stop any active (pac) decoders */
+				close(fd);
+				fd = -1;
+			}
+			a[0].op = CHANNOP;
+			switch(newstate.cmd){
+			default:
+				sysfatal("pacproc: unexpected newstate %d", newstate.cmd);
+			case Stop:
+				/* Wait for state to change */
+				break;
+			case Skip:
+			case Play:
+				fd = startplay(newstate.off);
+				if(fd >=0){
+					playstate = newstate;
+					a[0].op = CHANRCV;
+					continue;	/* Start reading */
+				}
+				newstate.cmd = Stop;
+			}
+			pb = recvp(spare);
+			pb->m = newstate.m;
+			sendp(full, pb);
+			playstate = newstate;
+		}
+	}
+}
+
+void
+pcmproc(void*)
+{
+	Pmsg localstate, newstate, prevstate;
+	int fd, n;
+	Pacbuf *pb, *b;
+	Alt a[3] = {
+		{full, &pb, CHANRCV},
+		{playout, &pb, CHANRCV},
+		{nil, nil, CHANEND},
+	};
+
+	/*
+	 * This is the real-time proc.
+	 * It gets its input from two sources, full data/control buffers from the pacproc
+	 * which mixes decoded data with control messages, and data buffers from the pcmproc's
+	 * (*this* proc's) own internal playout buffer.
+	 * When a command is received on the `full' channel containing a command that warrants
+	 * an immediate change of audio source (e.g., to silence or to another number), we just
+	 * toss everything in the pipeline -- i.e., the playout channel
+	 * Finally, we report all state changes using `playupdate' (another message channel)
+	 */
+	threadsetname("pcmproc");
+	close(srvfd[1]);
+	fd = open("/dev/audio", OWRITE);
+	if (fd < 0)
+		sysfatal("/dev/audio: %r");
+	localstate.cmd = 0;	/* Force initial playupdate */
+	newstate.cmd = Stop;
+	newstate.off = 0;
+//	rtsched();
+	for(;;){
+		if(newstate.m != localstate.m){
+			playupdate(newstate, nil);
+			localstate = newstate;
+		}
+		switch(alt(a)){
+		case 0:
+			/* buffer received from pacproc */
+			if((debug & DbgPcm) && localstate.m != prevstate.m){
+				fprint(2, "pcm, full: %s-%d, local state is %s-%d\n",
+					statetxt[pb->cmd], pb->off,
+					statetxt[localstate.cmd], localstate.off);
+				prevstate.m = localstate.m;
+			}
+			switch(pb->cmd){
+			default:
+				sysfatal("pcmproc: unknown newstate: %s-%d", statetxt[pb->cmd], pb->off);
+			case Resume:
+				a[1].op = CHANRCV;
+				newstate.cmd = Play;
+				break;
+			case Pause:
+				a[1].op = CHANNOP;
+				newstate.cmd = Pause;
+				break;
+			case Stop:
+				/* Dump all data in the buffer */
+				while(b = nbrecvp(playout))
+					if(b->cmd == Error){
+						playupdate(b->Pmsg, b->data);
+						sendp(spare, b);
+					}else
+						sendp(empty, b);
+				newstate.m = pb->m;
+				a[1].op = CHANRCV;
+				break;
+			case Skip:
+				/* Dump all data in the buffer, then fall through */
+				while(b = nbrecvp(playout))
+					if(b->cmd == Error){
+						playupdate(pb->Pmsg, pb->data);
+						sendp(spare, pb);
+					}else
+						sendp(empty, b);
+				a[1].op = CHANRCV;
+				newstate.cmd = Play;
+			case Error:
+			case Play:
+				/* deal with at playout, just requeue */
+				sendp(playout, pb);
+				pb = nil;
+				localstate = newstate;
+				break;
+			}
+			/* If we still have a buffer, free it */
+			if(pb)
+				sendp(spare, pb);
+			break;
+		case 1:
+			/* internal buffer */
+			if((debug & DbgPlayer) && localstate.m != prevstate.m){
+				fprint(2, "pcm, playout: %s-%d, local state is %s-%d\n",
+					statetxt[pb->cmd], pb->off,
+					statetxt[localstate.cmd], localstate.off);
+				prevstate.m = localstate.m;
+			}
+			switch(pb->cmd){
+			default:
+				sysfatal("pcmproc: unknown newstate: %s-%d", statetxt[pb->cmd], pb->off);
+			case Error:
+				playupdate(pb->Pmsg, pb->data);
+				localstate = newstate;
+				sendp(spare, pb);
+				break;
+			case Play:
+				/* play out this buffer */
+				totbytes += pb->len;
+				totbuffers++;
+				n = write(fd, pb->data, pb->len);
+				if (n != pb->len){
+					if (debug & DbgPlayer)
+						fprint(2, "pcmproc: file %d: %r\n", pb->off);
+					if (n < 0)
+						sysfatal("pcmproc: write: %r");
+				}
+				newstate.m = pb->m;
+				sendp(empty, pb);
+				break;
+			}
+			break;
+		}
+	}
+}
+
+void
+playinit(void)
+{
+	int i;
+
+	full = chancreate(sizeof(Pacbuf*), 1);
+	empty = chancreate(sizeof(Pacbuf*), NPacbuf);
+	spare = chancreate(sizeof(Pacbuf*), NSparebuf);
+	playout = chancreate(sizeof(Pacbuf*), NPacbuf+NSparebuf);
+	for(i = 0; i < NPacbuf; i++)
+		sendp(empty, malloc(sizeof(Pacbuf)));
+	for(i = 0; i < NSparebuf; i++)
+		sendp(spare, malloc(sizeof(Pacbuf)));
+
+	playc = chancreate(sizeof(Pmsg), 1);
+	procrfork(pacproc, nil, 32*1024, RFFDG);
+	procrfork(pcmproc, nil, 32*1024, RFFDG);
+}
+
+char *
+getplaystat(char *p, char *e)
+{
+	p = seprint(p, e, "empty buffers %d of %d\n", empty->n, empty->s);
+	p = seprint(p, e, "full buffers %d of %d\n", full->n, full->s);
+	p = seprint(p, e, "playout buffers %d of %d\n", playout->n, playout->s);
+	p = seprint(p, e, "spare buffers %d of %d\n", spare->n, spare->s);
+	p = seprint(p, e, "bytes %lud / buffers %lud played\n", totbytes, totbuffers);
+	return p;
+}

+ 150 - 0
sys/src/games/music/playlistfs/playlist.h

@@ -0,0 +1,150 @@
+typedef struct Worker Worker;
+typedef struct Req Req;
+typedef struct Fid Fid;
+typedef struct File File;
+typedef struct Playlist Playlist;
+typedef struct Wmsg Wmsg;
+typedef union Pmsg Pmsg;
+typedef struct Pacbuf Pacbuf;
+
+enum {
+	Qdir,
+	Qplayctl,
+	Qplaylist,
+	Qplayvol,
+	Qplaystat,
+	Nqid,
+};
+
+enum {
+	DbgPcm		= 0x01000,
+	DbgPac		= 0x02000,
+	DbgFs		= 0x10000,
+	DbgWorker	= 0x20000,
+	DbgPlayer	= 0x40000,
+	DbgError	= 0x80000,
+};
+
+enum {
+	Messagesize = 8*1024+IOHDRSZ,
+	Undef = 0x80000000,
+	/* 256 buffers of 4096 bytes represents 5.9 seconds
+	 * of playout at 44100 Hz (2*16bit samples)
+	 */
+	NPacbuf = 256,
+	Pacbufsize = 4096,
+	NSparebuf = 16,	/* For in-line commands (Pause, Resume, Error) */
+};
+
+enum {
+	/* Named commands (see fs.c): */
+	Nostate,	// can't use zero for state
+	Error,
+	Stop,
+	Pause,
+	Play,
+	Resume,
+	Skip,
+	/* Unnamed commands */
+	Work,
+	Check,
+	Flush,
+	Prep,
+	Preq,
+};
+
+union Pmsg {
+	ulong m;
+	struct{
+		ushort cmd;
+		ushort off;
+	};
+};
+
+struct Wmsg {
+	Pmsg;
+	void	*arg;	/* if(cmd != Work) mallocated by sender, freed by receiver */
+};
+
+struct Playlist {
+	/* The play list consists of a sequence of {objectref, filename}
+	 * entries.  Object ref and file name are separated by a tab.
+	 * An object ref may not contain a tab.  Entries are seperated
+	 * by newline characters.  Neither file names, nor object refs
+	 * may contain newlines.
+	 */
+	ulong	*lines;
+	ulong	nlines;
+	char	*data;
+	ulong	ndata;
+};
+
+struct File {
+	QLock;
+	Dir	dir;
+	Channel	*workers;
+	void	*data;
+};
+
+struct Worker
+{
+	Req	*r;
+	Channel	*eventc;
+};
+
+struct Fid
+{
+	QLock;
+	int	fid;
+	File	*file;
+	ushort	flags;
+	short	readers;
+	ulong	vers;	/* set to file's version when completely read */
+	Fid	*next;
+};
+
+struct Req
+{
+	uchar	indata[Messagesize];
+	uchar	outdata[Messagesize];
+	Fcall	ifcall;
+	Fcall	ofcall;
+	Fid*	fid;
+};
+
+struct Pacbuf {
+	Pmsg;
+	int len;
+	char data[Pacbufsize];
+};
+
+void	allocwork(Req*);
+Wmsg	waitmsg(Worker*, Channel*);
+int	sendmsg(Channel*, Wmsg*);
+void	bcastmsg(Channel*, Wmsg*);
+void	reqfree(Req*);
+Req	*reqalloc(void);
+void	readbuf(Req*, void*, long);
+void	readstr(Req*, char*);
+void	volumeset(int *v);
+void	playupdate(Pmsg, char*);
+void	playinit(void);
+void	volumeproc(void*);
+void	srv(void *);
+long	robustread(int, void*, long);
+void	volumeupdate(int*);
+char	*getplaylist(int);
+char	*getplaystat(char*, char*);
+
+extern int		debug, aflag;
+extern char	*user;
+extern Channel	*playc;
+extern char	*statetxt[];
+extern int		volume[8];
+extern Playlist	playlist;
+extern Channel	*workers;
+extern Channel	*volumechan;
+extern Channel	*playchan;
+extern Channel	*playlistreq;
+extern File	files[];
+extern int		srvfd[];

+ 20 - 0
sys/src/games/music/playlistfs/playplumb.c

@@ -0,0 +1,20 @@
+#include <u.h>
+#include <libc.h>
+
+void
+main(int argc, char *argv[])
+{
+	Plumbmsg *m;
+	int fd;
+
+	fd = plumbopen("audioplay", OREAD);
+	if (fd < 0)
+		sysfatal("port audioplay: %r");
+	for (;;) {
+		m = plumbrecv(fd);
+		if (m == nil)
+			sysfatal("plumrecv: %r");
+		
+		plumbfree(m);
+	}
+}

+ 92 - 0
sys/src/games/music/playlistfs/volume.c

@@ -0,0 +1,92 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <fcall.h>
+#include "pool.h"
+#include "playlist.h"
+
+int	minvolume, maxvolume;
+
+void
+volumeproc(void *)
+{
+	int fd, n, nf, i, nlines;
+	static char buf[1024];
+	char *lines[32];
+	char *fields[8];
+	char *subfields[9];
+	int volume[8], nvolumes;
+
+	threadsetname("volumeproc");
+	close(srvfd[1]);
+	fd = open("/dev/audioctl", OREAD);
+	if (fd < 0)
+		threadexits(nil);
+	for(;;){
+		n = read(fd, buf, sizeof buf -1);
+		if (n == 0)
+			continue;
+		if (n < 0){
+			fprint(2, "volumeproc: read: %r\n");
+			threadexits("volumeproc");
+		}
+		buf[n] = '\0';
+		nlines = getfields(buf, lines, nelem(lines), 1, "\n");
+		for(i = 0; i < nlines; i++){
+			nf = tokenize(lines[i], fields, nelem(fields));
+			if (nf == 0)
+				continue;
+			if (nf != 6 || strcmp(fields[0], "volume") || strcmp(fields[1], "out"))
+				continue;
+			minvolume = strtol(fields[3], nil, 0);
+			maxvolume = strtol(fields[4], nil, 0);
+			if (minvolume >= maxvolume)
+				continue;
+			nvolumes = tokenize(fields[2], subfields, nelem(subfields));
+			if (nvolumes <= 0 || nvolumes > 8)
+				sysfatal("bad volume data");
+			if (debug)
+				fprint(2, "volume changed to '");
+			for (i = 0; i < nvolumes; i++){
+				volume[i] = strtol(subfields[i], nil, 0);
+				volume[i]= 100*(volume[i]- minvolume)/(maxvolume-minvolume);
+				if (debug)
+					fprint(2, " %d", volume[i]);
+			}
+			if (debug)
+				fprint(2, "'\n");
+			while (i < 8)
+				volume[i++] = Undef;
+			send(volumechan, volume);
+		}
+	}
+}
+
+void
+volumeset(int *v)
+{
+	int fd, i;
+	char buf[256], *p;
+
+	fd = open("/dev/audioctl", OWRITE);
+	if (fd < 0){
+		fd = open("/dev/volume", OWRITE);
+		if (fd < 0){
+			fprint(2, "Can't set volume: %r");
+			return;
+		}
+		fprint(fd, "audio out %d",  v[0]);
+		v[1] = Undef;
+		send(volumechan, volume);
+	} else {
+		p = buf;
+		for (i = 0; i < 8; i++){
+			if (v[i] == Undef) break;
+			p = seprint(p, buf+sizeof buf, (p==buf)?"volume out '%d":" %d",
+				minvolume + v[i] * (maxvolume-minvolume) / 100);
+		}
+		p = seprint(p, buf+sizeof buf, "'\n");
+		write(fd, buf, p-buf);
+	}
+	close(fd);
+}

+ 46 - 0
sys/src/games/music/readcd

@@ -0,0 +1,46 @@
+#!/bin/rc
+
+cdrom=/dev/sdC1
+
+switch($#*){
+case 3
+	starttrack = `{echo $1 - 1 | hoc}
+	endtrack = `{echo $2 - 1 | hoc}
+	desttrack = $3
+case *
+	echo Usage readcd starttrack endtrack desttrack
+}
+
+if(test -e /mnt/cd/ctl){
+	echo -n ingest >/mnt/cd/ctl >[2]/dev/null
+}
+if not {
+	if (~ $cdrom '')	cdfs
+	if not		cdfs -d $cdrom
+}
+
+>/tmp/readcd
+>/tmp/map
+cat /mnt/cd/ctl
+sed 1q /mnt/cd/ctl | rc
+echo $starttrack $endtrack $desttrack | awk '{
+	start = $1
+	finish = $2
+	dest = $3
+	print "read cd tracks " start "-" finish " starting at " dest
+	for (i = start; i <= finish; i++) {
+		cmd = sprintf("ls -l /mnt/cd/a%3.3d | awk ''{print $6}''>>/tmp/readcd", i)
+		system(cmd)
+		getline x<"/tmp/readcd"
+		sec = x/44100/4
+		min = sec/60
+		sec = sec%60
+		printf("track {\n\t\n\tfile {%3.3d}\n\ttime {%d:%2.2d}\n}\n",i+dest-start,min,sec)>"/tmp/map"
+	}
+	for (i = start; i <= finish; i++) {
+		cmd = sprintf("/bin/games/pacenc /mnt/cd/a%3.3d %3.3d",i,i+dest-start)
+		print cmd
+		system(cmd)
+	}
+}'
+echo eject >/mnt/cd/ctl