Linux-声音编程教程-九-

11 阅读24分钟

Linux 声音编程教程(九)

原文:Linux Sound Programming

协议:CC BY-NC-SA 4.0

二十八、TiMidity 和 Karaoke

TiMidity 是 MIDI 播放器,不是 Karaoke 播放器。它被设计成一个具有特殊可扩展性的独立应用。开箱后,它可以播放 Karaoke,但不是很好。这一章着眼于如何与 TiMidity 建立一个 Karaoke 系统。

默认情况下,它只播放 MIDI 音乐,歌词打印出来。

$timidity ../54154.mid
Requested buffer size 32768, fragment size 8192
ALSA pcm 'default' set buffer size 32768, period size 8192 bytes
Playing ../54154.mid
MIDI file: ../54154.mid
Format: 1  Tracks: 1  Divisions: 30
No instrument mapped to tone bank 0, program 92 - this instrument will not be heard
#0001
@@00@12
@Here Comes The Sun
@
@@Beatles
Here comes the sun
doo doo doo doo
Here comes the sun
I said it's alright
Little
darling

但是它有许多可供选择的界面,提供不同的显示。如果您使用-h(帮助)选项运行timidity,它将显示一个类似这样的屏幕:

Available interfaces (-i, --interface option):
  -in          ncurses interface
  -ie          Emacs interface (invoked from `M-x timidity')
  -ia          XAW interface
  -id          dumb interface
  -ir          remote interface
  -iA          ALSA sequencer interface

默认的界面是“哑的”,但是如果你运行 Xaw 界面,你会得到如图 28-1 所示的显示。

A435426_1_En_28_Fig1_HTML.jpg

图 28-1。

TiMidity with Xaw interface

然而,有一个不幸的影响:歌词在播放之前就被显示了!要让歌词像应该唱的那样播放,您需要打开--trace选项。手册页中的“切换跟踪模式。在跟踪模式下,TiMidity++试图实时显示其当前状态。(您可能会发现文档和行为之间的联系不太明显。)

timidity --trace ../54154.mid

这对于 MIDI 文件来说已经很好了;歌词在该唱的时候显示。但是它不显示 KAR 文件的歌词。为此,您需要--trace-text-meta选项。

timidity --trace --trace-text-meta ../54154.kar

于是,到了这个阶段,TiMidity 对于 Karaoke 文件(以及带有歌词事件的 MIDI 文件)会将歌词实时显示在屏幕上。要对这个显示有自己的控制,你需要建立自己的 TiMidity 界面。

TiMidity 和 Jack

在第十七章中,我讨论了使用 Jack 播放 MIDI 文件。Jack 设计用于连接任意配置的音频源和接收器。例如,通过运行qjackctl,您可以将麦克风输出链接到扬声器输入。这是通过拖动capture_1playback_1来实现的,以此类推,看起来就像图 28-2 。

A435426_1_En_28_Fig2_HTML.jpg

图 28-2。

qjackctl showing microphone to speakers

如果 TiMidity,然后运行与插孔输出,你得到即时 Karaoke。您还可以使用--trace选项查看实时播放的歌词。

timidity -Oj --trace 54154.mid

连接如图 28-3 中的qjackctl所示。

A435426_1_En_28_Fig3_HTML.jpg

图 28-3。

qjackctl showing TiMidity

歌词显示很蹩脚,以后会改进。

TiMidity 界面

你需要从 SourceForge Timothy ++下载 TiMidity 源代码( http://sourceforge.net/projects/timidity/?source=dlp )。

在第二十一章中,我讨论了两种使用 TiMidity 来构建应用的方法。

  • 你可以 TiMidity 的搭建一个前端作为库后端。
  • 你可以使用带有定制接口的标准 TiMidity 作为 TiMidity 的后端。

这两种选择在这里都是可能的,但有一个问题:如果你想捕捉 MIDI 事件,那么你必须这样做作为 TiMidity 的后端,这需要你建立一个 TiMidity 的接口。

概括地说,TiMidity 的不同接口文件存储在目录interface中,并且包括像哑接口的dumb_c.c这样的文件。它们都围绕着一个在timidity/controls.h中定义的数据结构ControlMode

typedef struct {
  char *id_name, id_character;
  char *id_short_name;
  int verbosity, trace_playing, opened;

  int32 flags;

  int  (*open)(int using_stdin, int using_stdout);
  void (*close)(void);
  int (*pass_playing_list)(int number_of_files, char *list_of_files[]);
  int  (*read)(int32 *valp);
  int  (*write)(char *buf, int32 size);
  int  (*cmsg)(int type, int verbosity_level, char *fmt, ...);
  void (*event)(CtlEvent *ev);  /* Control events */
} ControlMode;

对于该结构中函数的最简单值,参见interface/dumb_c.c中哑接口的代码。

对于处理歌词,要设置的主要字段是函数event()。这将被传递一个指向在timidity/controls.h中定义的CtlEvent的指针。

typedef struct _CtlEvent {
    int type;           /* See above */
    ptr_size_t v1, v2, v3, v4;/* Event value */
} CtlEvent;

类型字段区分大量事件类型,如CTLE_NOW_LOADINGCTLE_PITCH_BEND。你感兴趣的类型是CTLE_LYRIC

处理这个问题的典型代码在interface/dumb_c.c中,它将事件信息打印到输出中。

static void ctl_event(CtlEvent *e)
{
    switch(e->type) {
      case CTLE_LYRIC:
        ctl_lyric((int)e->v1);
        break;
   }
}

static void ctl_lyric(int lyricid)
{
    char *lyric;

    lyric = event2string(lyricid);
    if(lyric != NULL)
    {
        if(lyric[0] == ME_KARAOKE_LYRIC)
        {
            if(lyric[1] == '/' || lyric[1] == '\\')
            {
                fprintf(outfp, "\n%s", lyric + 2);
                fflush(outfp);
            }
            else if(lyric[1] == '@')
            {
                if(lyric[2] == 'L')
                    fprintf(outfp, "\nLanguage: %s\n", lyric + 3);
                else if(lyric[2] == 'T')
                   fprintf(outfp, "Title: %s\n", lyric + 3);
                else
                    fprintf(outfp, "%s\n", lyric + 1);
            }
            else
            {
                fputs(lyric + 1, outfp);
                fflush(outfp);
            }
        }
        else
        {
            if(lyric[0] == ME_CHORUS_TEXT || lyric[0] == ME_INSERT_TEXT)
                fprintf(outfp, "\r");
            fputs(lyric + 1, outfp);
            fflush(outfp);
        }
    }
}

获取歌词列表

当前界面在 Karaoke 方面的缺点是,虽然它们可以在播放时显示歌词,但是它们不显示歌词线,并且在播放时逐渐突出显示它们。为此,你需要一套歌词。

TiMidity 实际上建立了一个歌词列表,并使它们易于理解。它有一个函数event2string(),接受从 1 开始的整数参数。对于每个值,它返回一个歌词或文本事件的字符串,最后返回列表末尾的NULL。返回的第一个字符是类型参数;剩下的就是字符串了。使用 GLib 函数,您可以使用以下内容为 KAR 文件构建一个行数组:

struct _lyric_t {
    gchar *lyric;
    long tick; // not used here
};
typedef struct _lyric_t lyric_t;

struct _lyric_lines_t {
    char *language;
    char *title;
    char *performer;
    GArray *lines; // array of GString *
};
typedef struct _lyric_lines_t lyric_lines_t;

GArray *lyrics;
lyric_lines_t lyric_lines;

static void build_lyric_lines() {
    int n;
    lyric_t *plyric;
    GString *line = g_string_new("");
    GArray *lines =  g_array_sized_new(FALSE, FALSE, sizeof(GString *), 64);

    lyric_lines.title = NULL;

    n = 1;
    char *evt_str;
    while ((evt_str = event2string(n++)) != NULL) {
        gchar *lyric = evt_str+1;

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'L')) {
            lyric_lines.language =  lyric + 2;
            continue;
        }

        if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'T')) {
            if (lyric_lines.title == NULL) {
                lyric_lines.title = lyric + 2;
            } else {
                lyric_lines.performer = lyric + 2;
            }
            continue;
        }

        if (lyric[0] == '@') {
            // some other stuff like @KMIDI KARAOKE FILE
            continue;
        }

        if ((lyric[0] == '/') || (lyric[0] == '\\')) {
            // start of a new line
            // add to lines
            g_array_append_val(lines, line);
            line = g_string_new(lyric + 1);
        }  else {
            line = g_string_append(line, lyric);
        }
    }
    lyric_lines.lines = lines;

    printf("Title is %s, performer is %s, language is %s\n",
           lyric_lines.title, lyric_lines.performer, lyric_lines.language);
    for (n = 0; n < lines->len; n++) {
        printf("Line is %s\n", g_array_index(lines, GString *, n)->str);
    }
}

函数build_lyric_lines()应该从ctl_event()CTLE_LOADING_DONE分支调用。

TiMidity 选项

如果你选择使用 TiMidity 作为前端,那么你需要用合适的选项来运行它。这些包括打开跟踪和动态加载新接口。例如,对于当前目录中的“v”接口,这可以通过以下方式实现:

timidity -d. -iv --trace  --trace-text-meta ...
.

另一种方法是构建一个主程序,将 TiMidity 作为一个库。TiMidity 的命令行参数必须作为硬编码参数包含在应用中。一个简单的方法是:CtlMode有一个字段trace_playing,将它设置为 1 可以打开跟踪。将文本事件作为歌词事件需要更深入地挖掘 TiMidity,但只需要(在初始化库后不久)以下内容:

extern int opt_trace_text_meta_event;
opt_trace_text_meta_event = 1;

使用 Pango + Cairo + Xlib 播放歌词

我希望能够在 Raspberry Pi 和类似的片上系统(SOC)上播放我的 Karaoke 文件。不幸的是,Raspberry Pi 的 CPU 性能严重不足,所以我最终使用了 CubieBoard 2。

在这种 CPU 上,任何涉及大量图形的东西都是不可能的。所有的 MIDI 播放器都达到了接近(或超过)100%的 CPU 使用率。因此,下一节中讨论的显示背景视频的系统在不使用 GPU 的情况下在 Raspberry Pi 上是不可行的,这在我的书《Raspberry Pi GPU 音频视频编程》中讨论过。续集中讨论的程序可以在任何现有的笔记本电脑和台式机上正常运行。

在这一节中,你使用 TiMidity 作为 MIDI 播放器,使用最小的后端来显示播放的歌词。使用最低级别的 GUI 支持,即 Xlib。这可用于使用低级 Xlib 调用(如XDrawImageString)来绘制文本。这适用于 ASCII 语言,通过适当的字体选择,也适用于 ISO-8859 系列中的其他语言。

亚洲语言在标准 c 中更难处理。当使用像 UTF-8 这样的编码时,它们包含 1 或 2 字节的字符。要管理它们,最简单的方法是切换到专门处理它们的库,比如 Cairo。

Cairo 适合绘制简单的文本。例如,对于汉字,你必须找到一种能让你画出它们的字体。或者,你可以再跳一级到盘古。Pango 处理所有的字体问题,并生成发送到 X 服务器的字形。

下面的接口x_code.c采用了这种方法。

当然,前面的 naive interface 部分和本部分的 Xlib 接口之间的本质区别在于绘图。函数build_lyric_lines给你一组要渲染的线。Pango 和 Cairo 需要以下附加数据类型:

GArray *lyrics;
GString *lyrics_array[NUM_LINES];

lyric_lines_t lyric_lines;

typedef struct _coloured_line_t {
    gchar *line;
    gchar *front_of_line;
    gchar *marked_up_line;
    PangoAttrList *attrs;
} coloured_line_t;

int height_lyric_pixbufs[] = {100, 200, 300, 400}; // vertical offset of lyric in video
int coloured_text_offset;

// int current_panel = 1;  // panel showing current lyric line
int current_line = 0;  // which line is the current lyric
gchar *current_lyric;   // currently playing lyric line
GString *front_of_lyric;  // part of lyric to be coloured red
//GString *end_of_lyric;    // part of lyrci to not be coloured

gchar *markup[] = {"<span font=\"28\" foreground=\"RED\">",
                   "</span><span font=\"28\" foreground=\"white\">",
                   "</span>"};
gchar *markup_newline[] = {"<span foreground=\"black\">",
                           "</span>"};
GString *marked_up_label;

PangoFontDescription *font_description;

cairo_surface_t *surface;
cairo_t *cr;

标记字符串将用红色绘制已播放的文本,用白色绘制未播放的文本,而markup_newline将清除前一行。主要绘图功能如下:

static void paint_background() {
    cr = cairo_create(surface);
    cairo_set_source_rgb(cr, 0.0, 0.8, 0.0);
    cairo_paint(cr);
    cairo_destroy(cr);
}

static void set_font() {
    font_description = pango_font_description_new ();
    pango_font_description_set_family (font_description, "serif");
    pango_font_description_set_weight (font_description, PANGO_WEIGHT_BOLD);
    pango_font_description_set_absolute_size (font_description, 32 * PANGO_SCALE);
}

static int draw_text(char *text, float red, float green, float blue, int height, int offset) {
  // See http://cairographics.org/FAQ/
  PangoLayout *layout;
  int width, ht;
  cairo_text_extents_t extents;

  layout = pango_cairo_create_layout (cr);
  pango_layout_set_font_description (layout, font_description);
  pango_layout_set_text (layout, text, -1);

  if (offset == 0) {
      pango_layout_get_size(layout, &width, &ht);
      offset = (WIDTH - (width/PANGO_SCALE)) / 2;
  }

  cairo_set_source_rgb (cr, red, green, blue);
  cairo_move_to (cr, offset, height);
  pango_cairo_show_layout (cr, layout);

  g_object_unref (layout);
  return offset;
}

初始化 X 和 Cairo 的函数如下:

static void init_X() {
    int screenNumber;
    unsigned long foreground, background;
    int screen_width, screen_height;
    Screen *screen;
    XSizeHints hints;
    char **argv = NULL;
    XGCValues gcValues;
    Colormap colormap;
    XColor rgb_color, hw_color;
    Font font;
    //char *FNAME = "hanzigb24st";
    char *FNAME = "-misc-fixed-medium-r-normal--0-0-100-100-c-0-iso10646-1";

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Can't open dsplay\n");
        exit(1);
    }
    screenNumber = DefaultScreen(display);
    foreground = BlackPixel(display, screenNumber);
    background = WhitePixel(display, screenNumber);

    screen = DefaultScreenOfDisplay(display);
    screen_width = WidthOfScreen(screen);
    screen_height = HeightOfScreen(screen);

    hints.x = (screen_width - WIDTH) / 2;
    hints.y = (screen_height - HEIGHT) / 2;
    hints.width = WIDTH;
    hints.height = HEIGHT;
    hints.flags = PPosition | PSize;

    window = XCreateSimpleWindow(display,
                                 DefaultRootWindow(display),
                                 hints.x, hints.y, WIDTH, HEIGHT, 10,
                                 foreground, background);

    XSetStandardProperties(display, window,
                           "TiMidity", "TiMidity",
                           None,
                           argv, 0,
                           &hints);

    XMapWindow(display, window);

    set_font();
    surface = cairo_xlib_surface_create(display, window,
                                        DefaultVisual(display, 0), WIDTH, HEIGHT);
    cairo_xlib_surface_set_size(surface, WIDTH, HEIGHT);

    paint_background();

    /*
    cr = cairo_create(surface);
    draw_text(g_array_index(lyric_lines.lines, GString *, 0)->str,
              0.0, 0.0, 1.0, height_lyric_pixbufs[0]);
    draw_text(g_array_index(lyric_lines.lines, GString*, 1)->str,
              0.0, 0.0, 1.0, height_lyric_pixbufs[0]);
    cairo_destroy(cr);
    */
    XFlush(display);
}

关键函数是ctl_lyric,负责处理播放的歌词。如果歌词信号和行尾带有\/,那么它必须更新current_line。接下来的几行重新绘制每一行的文本,然后逐步遍历当前行,将第一部分涂成红色,其余部分涂成白色。

static void ctl_lyric(int lyricid)
{
    char *lyric;

    current_file_info = get_midi_file_info(current_file, 1);

    lyric = event2string(lyricid);
    if(lyric != NULL)
        lyric++;
    printf("Got a lyric %s\n", lyric);

    if ((*lyric == '\\') || (*lyric == '/')) {

        int next_line = current_line + NUM_LINES;
        gchar *next_lyric;

        if (current_line + NUM_LINES < lyric_lines.lines->len) {
            current_line += 1;

            // update label for next line after this one
            next_lyric = g_array_index(lyric_lines.lines, GString *, next_line)->str;

        } else {
            current_line += 1;
            lyrics_array[(next_line-1) % NUM_LINES] = NULL;
            next_lyric = "";
        }

        // set up new line as current line
        if (current_line < lyric_lines.lines->len) {
            GString *gstr = g_array_index(lyric_lines.lines, GString *, current_line);
            current_lyric = gstr->str;
            front_of_lyric = g_string_new(lyric+1); // lose     slosh
        }
        printf("New line. Setting front to %s end to \"%s\"\n", lyric+1, current_lyric);

        // Now draw stuff
        paint_background();

        cr = cairo_create(surface);

        int n;
        for (n = 0; n < NUM_LINES; n++) {

            if (lyrics_array[n] != NULL) {
                draw_text(lyrics_array[n]->str,
                          0.0, 0.0, 0.5, height_lyric_pixbufs[n], 0);
            }
        }
        // redraw current and next lines
        if (current_line < lyric_lines.lines->len) {
            if (current_line >= 2) {
                // redraw last line still in red
                GString *gstr = lyrics_array[(current_line-2) % NUM_LINES];
                if (gstr != NULL) {
                    draw_text(gstr->str,
                              1.0, 0.0, 00,
                              height_lyric_pixbufs[(current_line-2) % NUM_LINES],
                              0);
                }
            }
            // draw next line in brighter blue
            coloured_text_offset = draw_text(lyrics_array[(current_line-1) % NUM_LINES]->str,
                      0.0, 0.0, 1.0, height_lyric_pixbufs[(current_line-1) % NUM_LINES], 0);
            printf("coloured text offset %d\n", coloured_text_offset);
        }

        if (next_line < lyric_lines.lines->len) {
            lyrics_array[(next_line-1) % NUM_LINES] =
                g_array_index(lyric_lines.lines, GString *, next_line);
        }

        cairo_destroy(cr);
        XFlush(display);

    } else {
        // change text colour as chars are played
        if ((front_of_lyric != NULL) && (lyric != NULL)) {
            g_string_append(front_of_lyric, lyric);
            char *s = front_of_lyric->str;
            //coloured_lines[current_panel].front_of_line = s;

            cairo_t *cr = cairo_create(surface);

            // See http://cairographics.org/FAQ/
            draw_text(s, 1.0, 0.0, 0.0,
                      height_lyric_pixbufs[(current_line-1) % NUM_LINES],
                      coloured_text_offset);

            cairo_destroy(cr);
            XFlush(display);

        }
    }
}

文件x_code.c编译如下:

CFLAGS =   $(shell pkg-config --cflags gtk+-$(V).0 libavformat libavcodec libswscale libavutil )  -ITiMidity++-2.14.0/timidity/ -ITiMidity++-2.14.0/utils

LIBS =  -lasound -l glib-2.0 $(shell pkg-config --libs gtk+-$(V).0  libavformat libavcodec libavutil libswscale) -lpthread -lX11

gcc  -fPIC $(CFLAGS) -c -o x_code.o x_code.c $(LIBS)
 gcc -shared -o if_x.so x_code.o $(LIBS)

同样,这使用了 TiMidity 的本地编译和构建版本,因为 Ubuntu 版本崩溃了。它使用以下命令运行:

A435426_1_En_28_Figa_HTML.jpg

TiMidity++-2.14.0/timidity/timidity -d. -ix --trace --trace-text-meta <KAR file>

用 Gtk 播放背景视频

在第二十七章,我讨论了一个在电影上显示歌词的程序。除了前面的考虑之外,应用的其余部分与 FluidSynth 的情况类似:构建一组歌词行,使用 Pango over Gtk pixbufs 显示它们,当新的歌词事件发生时,更新歌词行中相应的颜色。

所有的动态动作都需要在 TiMidity 的后端发生,尤其是在函数ctl_event中。其他部分如初始化 FFmpeg 和 Gtk 在使用标准 TiMidity 时也必须发生在后端。如果 TiMidity 被用作一个库,这种初始化可能发生在前面或后面。为了简单起见,您只需将它们全部放在文件video_code.c的后面。

与上一节一样,您有一些初始数据结构和值,并将有一个两行coloured_line_t的数组。

struct _lyric_t {
    gchar *lyric;
    long tick;

};
typedef struct _lyric_t lyric_t;

struct _lyric_lines_t {
    char *language;
    char *title;
    char *performer;
    GArray *lines; // array of GString *
};
typedef struct _lyric_lines_t lyric_lines_t;

GArray *lyrics;

lyric_lines_t lyric_lines;

typedef struct _coloured_line_t {
    gchar *line;
    gchar *front_of_line;
    gchar *marked_up_line;
    PangoAttrList *attrs;
#ifdef USE_PIXBUF
    GdkPixbuf *pixbuf;
#endif
} coloured_line_t;

coloured_line_t coloured_lines[2];

GtkWidget *image;

int height_lyric_pixbufs[] = {300, 400}; // vertical offset of lyric in video

int current_panel = 1;  // panel showing current lyric line
int current_line = 0;  // which line is the current lyric
gchar *current_lyric;   // currently playing lyric line
GString *front_of_lyric;  // part of lyric to be coloured red
//GString *end_of_lyric;    // part of lyrci to not be coloured

// Colours seem to get mixed up when putting a pixbuf onto a pixbuf
#ifdef USE_PIXBUF
#define RED blue
#else
#define RED red
#endif

gchar *markup[] = {"<span font=\"28\" foreground=\"RED\">",
                   "</span><span font=\"28\" foreground=\"white\">",
b                   "</span>"};
gchar *markup_newline[] = {"<span foreground=\"black\">",
                           "</span>"};
GString *marked_up_label;

现在基本上有两个代码块:一个用于在播放每个新歌词时保持彩色线条阵列的更新,另一个用于播放视频,彩色线条在顶部。第一个块有三个功能:markup_line用 HTML 标记准备一个字符串,update_line_pixbuf通过将 Pango 属性应用到标记行来创建一个新的 pixbuf,以及ctl_lyric,它在每个新的歌词事件时被触发。

前两个功能如下:

void markup_line(coloured_line_t *line) {
    GString *str =  g_string_new(markup[0]);
    g_string_append(str, line->front_of_line);
    g_string_append(str, markup[1]);
    g_string_append(str, line->line + strlen(line->front_of_line));
    g_string_append(str, markup[2]);
    printf("Marked up label \"%s\"\n", str->str);

    line->marked_up_line = str->str;
    // we have to free line->marked_up_line

    pango_parse_markup(str->str, -1,0, &(line->attrs), NULL, NULL, NULL);
    g_string_free(str, FALSE);
}

void update_line_pixbuf(coloured_line_t *line) {
    //return;
    cairo_surface_t *surface;
    cairo_t *cr;

    int lyric_width = 480;
    int lyric_height = 60;
    surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                          lyric_width, lyric_height);
    cr = cairo_create (surface);

    PangoLayout *layout;
    PangoFontDescription *desc;

    // draw the attributed text
    layout = pango_cairo_create_layout (cr);
    pango_layout_set_text (layout, line->line, -1);
    pango_layout_set_attributes(layout, line->attrs);

    // centre the image in the surface
    int width, height;
    pango_layout_get_pixel_size(layout,
                                &width,
                                &height);
    cairo_move_to(cr, (lyric_width-width)/2, 0);

    pango_cairo_update_layout (cr, layout);
    pango_cairo_show_layout (cr, layout);

    // pull the pixbuf out of the surface
    unsigned char *data = cairo_image_surface_get_data(surface);
    width = cairo_image_surface_get_width(surface);
    height = cairo_image_surface_get_height(surface);
    int stride = cairo_image_surface_get_stride(surface);
    printf("Text surface width %d height %d stride %d\n", width, height, stride);

    GdkPixbuf *old_pixbuf = line->pixbuf;
    line->pixbuf = gdk_pixbuf_new_from_data(data, GDK_COLORSPACE_RGB, 1, 8, width, height, stride, NULL, NULL);
    cairo_surface_destroy(surface);
    g_object_unref(old_pixbuf);
}

处理每个新歌词事件的函数需要确定是否发生了换行事件,即歌词是单个\字符时。然后,它需要更新索引current_line,并用新的一行替换先前的一行。一旦完成,对于所有事件,当前行被标记,并且它的位图被生成用于绘制。ctl_lyric功能如下:

static void ctl_lyric(int lyricid)
{
    char *lyric;

    current_file_info = get_midi_file_info(current_file, 1);

    lyric = event2string(lyricid);
    if(lyric != NULL)
        lyric++;
    printf("Got a lyric %s\n", lyric);
    if (*lyric == '\\') {
        int next_panel = current_panel; // really (current_panel+2)%2
        int next_line = current_line + 2;
        gchar *next_lyric;

        if (current_line + 2 >= lyric_lines.lines->len) {
            return;
        }
        current_line += 1;
        current_panel = (current_panel + 1) % 2;

        // set up new line as current line
        current_lyric = g_array_index(lyric_lines.lines, GString *, current_line)->str;
        front_of_lyric = g_string_new(lyric+1); // lose \
        printf("New line. Setting front to %s end to \"%s\"\n", lyric+1, current_lyric);

        coloured_lines[current_panel].line = current_lyric;
        coloured_lines[current_panel].front_of_line = lyric+1;
        markup_line(coloured_lines+current_panel);
#ifdef USE_PIXBUF
        update_line_pixbuf(coloured_lines+current_panel);
#endif
        // update label for next line after this one
        next_lyric = g_array_index(lyric_lines.lines, GString *, next_line)->str;

        marked_up_label = g_string_new(markup_newline[0]);

        g_string_append(marked_up_label, next_lyric);
        g_string_append(marked_up_label, markup_newline[1]);
        PangoAttrList *attrs;
        gchar *text;
        pango_parse_markup (marked_up_label->str, -1,0, &attrs, &text, NULL, NULL);

        coloured_lines[next_panel].line = next_lyric;
        coloured_lines[next_panel].front_of_line = "";
        markup_line(coloured_lines+next_panel);
        update_line_pixbuf(coloured_lines+next_panel);
    } else {
        // change text colour as chars are played
        if ((front_of_lyric != NULL) && (lyric != NULL)) {
            g_string_append(front_of_lyric, lyric);
            char *s = front_of_lyric->str;
            coloured_lines[current_panel].front_of_line = s;
            markup_line(coloured_lines+current_panel);
            update_line_pixbuf(coloured_lines+current_panel);
        }
    }
}

static gboolean draw_image(gpointer user_data) {
    GdkPixbuf *pixbuf = (GdkPixbuf *) user_data;

    gtk_image_set_from_pixbuf((GtkImage *) image, pixbuf);
    gtk_widget_queue_draw(image);
    g_object_unref(pixbuf);

    return G_SOURCE_REMOVE;
}

播放视频和覆盖彩色线条的功能没有什么本质上的新东西。它从视频中读取一帧,并将其放入 pixbuf。然后,对于每个歌词面板,它将彩色线绘制到 pixbuf 中。最后,它调用gdk_threads_add_idle以便 Gtk 可以在其主线程中绘制 pixbuf。功能play_background如下:

static void *play_background(void *args) {

    int i;
    AVPacket packet;
    int frameFinished;
    AVFrame *pFrame = NULL;

    int oldSize;
    char *oldData;
    int bytesDecoded;
    GdkPixbuf *pixbuf;
    AVFrame *picture_RGB;
    char *buffer;

    pFrame=av_frame_alloc();

    i=0;
    picture_RGB = avcodec_frame_alloc();
    buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, 720, 576));
    avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, 720, 576);

    int width = pCodecCtx->width;
    int height = pCodecCtx->height;

    sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                                               pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB24,
                                               SWS_BICUBIC, NULL, NULL, NULL);

    while(av_read_frame(pFormatCtx, &packet)>=0) {
        if(packet.stream_index==videoStream) {
            //printf("Frame %d\n", i++);
            usleep(33670);  // 29.7 frames per second
            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
                                  &packet);

            if (frameFinished) {
                //printf("Frame %d\n", i++);

                sws_scale(sws_ctx,  (uint8_t const * const *) pFrame->data, pFrame->linesize, 0,
                                                  pCodecCtx->height, picture_RGB->data, picture_RGB->linesize);

                pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB, 0, 8,
                                                  width, height, picture_RGB->linesize[0],
                                                  pixmap_destroy_notify, NULL);

                // Create the destination surface
                cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                                                       width, height);
                cairo_t *cr = cairo_create(surface);

                // draw the background image
                gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
                cairo_paint (cr);

                // draw the lyric
                GdkPixbuf *lyric_pixbuf = coloured_lines[current_panel].pixbuf;
                if (lyric_pixbuf != NULL) {
                    int width = gdk_pixbuf_get_width(lyric_pixbuf);
                    gdk_cairo_set_source_pixbuf(cr,
                                                lyric_pixbuf,
                                                (720-width)/2,
                                                height_lyric_pixbufs[current_panel]);
                    cairo_paint (cr);
                }

                int next_panel = (current_panel+1) % 2;
                lyric_pixbuf = coloured_lines[next_panel].pixbuf;
                if (lyric_pixbuf != NULL) {
                    int width = gdk_pixbuf_get_width(lyric_pixbuf);
                    gdk_cairo_set_source_pixbuf(cr,
                                                lyric_pixbuf,
                                                (720-width)/2,
                                                height_lyric_pixbufs[next_panel]);
                    cairo_paint (cr);
                }

                pixbuf = gdk_pixbuf_get_from_surface(surface,
                                                     0,
                                                     0,
                                                     width,
                                                     height);
                gdk_threads_add_idle(draw_image, pixbuf);

         /* reclaim memory */
                sws_freeContext(sws_ctx);
                g_object_unref(layout);
                cairo_surface_destroy(surface);
                cairo_destroy(cr);

            }
        }
        av_free_packet(&packet);
    }
    sws_freeContext(sws_ctx);

    printf("Video over!\n");
    exit(0);
}

它使用以下命令运行:

TiMidity++-2.14.0/timidity/timidity -d. -iv --trace --trace-text-meta <KAR file>

从外形上看,像图 28-4 。

A435426_1_En_28_Fig4_HTML.jpg

图 28-4。

Caption

以 TiMidity 为库的背景视频

其代码与第二十一章中的代码结构相同。它在文件gtkkaraoke_player_video_pango.c中。

#include <stdio.h>
#include <stdlib.h>

#include "sysdep.h"
#include "controls.h"

extern ControlMode  *video_ctl;
extern ControlMode  *ctl;

static void init_timidity() {
    int err;

    timidity_start_initialize();

    if ((err = timidity_pre_load_configuration()) != 0) {
        printf("couldn't pre-load configuration file\n");
        exit(1);
    }

    err += timidity_post_load_configuration();

    if (err) {
        printf("couldn't post-load configuration file\n");
        exit(1);
    }

    timidity_init_player();

    extern int opt_trace_text_meta_event;
    opt_trace_text_meta_event = 1;

    ctl = &video_ctl;
    //ctl->trace_playing = 1;
    //opt_trace_text_meta_event = 1;

}

#define MIDI_FILE "54154.kar"

static void *play_midi(void *args) {
    char *argv[1];
    argv[0] = MIDI_FILE;
    int argc = 1;

    timidity_play_main(argc, argv);

    printf("Audio finished\n");
    exit(0);
}

int main(int argc, char** argv)
{

    int i;

    /* TiMidity */
    init_timidity();
    play_midi(NULL);

    return 0;
}

以 TiMidity 为前端的背景视频

该接口需要构建为共享库,包含以下内容:

if_video.so: video_code.c
        gcc  -fPIC $(CFLAGS) -c -o video_code.o video_code.c $(LIBS)
        gcc -shared -o if_video.so video_code.o $(LIBS)

TiMidity 然后运行选项。

timidity -d. -iv --trace  --trace-text-meta

和以前一样,它崩溃了 Ubuntu 发行版的 TiMidity,但是在当前的 Linux 环境下,它可以很好地工作。

添加麦克风输入

在这个阶段,你有一个应用可以播放 MIDI 文件,播放背景电影,并在视频顶部显示突出显示的歌词。没有麦克风输入可以跟着唱。

跟着唱可以在这个应用中处理,也可以由外部进程处理。如果你想把它包含在当前的应用中,那么你必须为两个音频流构建一个混音器。Java 在 Java 声音包中实现了这一点,但在 C 语言中,您需要自己完成这一点。这可以在 ALSA 完成,但会涉及复杂的 ALSA 混频器代码。

Jack 可以轻松混合来自不同流程的音频。前面的部分展示了如何做到这一点。

一个长期的目标是包括得分,等等。然而,这将带你进入深度信号处理的领域(例如,使用 YIN 等算法识别唱的音符),这超出了本书的范围。

结论

这一章向你展示了如何在 Karaoke 系统中使用 MIDI 播放器。在我的笔记本电脑上,它使用了大约 35%的 Gtk 3.0 CPU。

二十九、Jack 和 Karaoke

插孔是为专业音响设计的。在这一章中,你将把前几章的技术应用到构建一个 Karaoke 系统中。

使用千斤顶架实现效果

Karaoke 从 MIDI 信号源和麦克风接收输入。这些混合在一起。一般来说,有一个整体音量控制,但通常也有一个麦克风音量控制。虽然 MIDI 源应该直接通过,但通常会对麦克风应用混响效果。

这些都是 LADSPA 模块可以提供的效果(参见第十四章)。Jack 应用jack-rack使这些插件可以被 Jack 应用访问,以便 LADSPA 效果可以被应用到 Jack 管道。

向会话中添加一个模块相当简单。点击+按钮,从巨大的效果菜单中选择。例如,从实用程序菜单中选择 Karaoke 插件,如图 29-1 所示。

A435426_1_En_29_Fig1_HTML.jpg

图 29-1。

Selecting Karaoke effect in Jack Rack

以下是一些可能相关的模块:

  • Karaoke(编号 1409),显示在工具菜单中。这将尝试从音乐轨道中移除中央人声。
  • 模拟器➤混响菜单中有许多混响模块。TAP 混响器似乎是功能最全的(但不保证实时)。
    • g 动词
    • 平板混响
    • 拍子混响器(来自拍子插件)The TAP Reverberator seems to be the most full featured (but is not guaranteed to be in real time).
  • 振幅➤放大器菜单中有许多放大器。

一个jack-rack应用可以应用多个模块,也可以运行多个应用。例如,将音量控制应用到麦克风,然后在将其发送到扬声器之前应用混响,可以通过添加抽头混响器和其中一个放大器来完成。这看起来像图 29-2 。

A435426_1_En_29_Fig2_HTML.jpg

图 29-2。

Jack Rack with reverb and amplifier plug-ins

我在 USB 声霸 TruStudioPro 上运行这个。这才 16 位,我好像找不到合适的插孔硬件配置。所以,我用一个插头设备手动运行 Jack,Jack 抱怨过这个设备,但不管怎样它还是工作了。

jackd -dalsa -dplughw:2 -r 48000

虽然gladish可以在它的插孔配置菜单下看到它,但我还没能让gladish接受声霸作为设置。到目前为止,我只能设法让 Jack 运行下作为一个插头设备,gladish不断交换回一个硬件设备。

qjackctl很好地保存和恢复会话,使用正确的插件和设置启动jack-rack,并将其链接到正确的捕获和回放端口。

播放 MIDI

主要合成器引擎 Timothy 和 FluidSynth 将输出到 ALSA 设备。为了让他们进入 Jack 世界,Jack 需要用-Xseq选项启动,或者需要运行a2jmidid

您可以尝试使用 Jack 会话管理器来管理连接(例如,qjackctl)。但这在使用 MIDI 合成器(如 TiMidity 或 FluidSynth)时遇到了障碍,因为它们假设 PulseAudio 输出而不是 Jack 输出。恢复会话无法恢复带有插孔输出的合成器。

您可以尝试使用 LADSPA 来管理连接。不幸的是,我至今无法使用gladish管理 Jack 服务器设置。因此,它使用默认的 ALSA 设置启动 Jack,而不使用-Xseq设置将 ALSA 端口映射到 Jack。你需要启动a2jmidid,它才能成功管理一个会话,比如timidityjack_keyboarda2jmidid

即使这样,连接图看起来还是一团乱(图 29-3 )。

A435426_1_En_29_Fig3_HTML.jpg

图 29-3。

LADISH playing MIDI

TiMidity 加上千斤顶架

在第二十八章中,你用一个插孔后端和一个 Xaw 接口的 TiMidity 给出了一个基本的 Karaoke 系统。你现在可以通过使用千斤顶效果来改善这一点。

  • 使用插孔输出和 Xaw 接口运行 TiMidity,并使用以下命令将歌词与声音同步:

    timidity -ia -B2,8 -Oj -EFverb=0 --trace --trace-text-meta
    
    
  • 运行安装了 TAP 混响器和音量控制的 Jack Rack。

  • 使用qjackctl连接端口。

最终的系统如图 29-4 所示。

A435426_1_En_29_Fig4_HTML.jpg

图 29-4。

TiMidity with Jack Rack

定制 TiMidity 构建

如果我试图动态加载另一个接口,Ubuntu 发行版的 TiMidity 版本会崩溃。随着代码被剥离,不可能找出原因。所以,要添加一个新的接口,你需要从源头上建立 TiMidity。

我现在使用的命令如下:

./configure --enable-audio=alsa,jack \
            --enable-interface=xaw,gtk \
            --enable-server \
            --enable-dynamic
make clean
make

一个带键的接口,比如说“k”,可以用如下的 Jack 输出来运行:

timidity -d. -ik -Oj --trace  --trace-text-meta 54154.mid

使用插孔架音高移位播放 MP3+G

播放器 VLC 将播放 MP3+G 文件。通常 MP3+G 是一个压缩文件,包含一个 MP3 文件和一个 CDG 文件,它们有相同的根目录。这必须解压缩,然后可以播放给 VLC 的 MP3 文件名。

vlc file.mp3

这将获得 CDG 文件并显示歌词。

VLC 可与带--aout jack选项的千斤顶一起使用。

vlc --aout jack file.mp3

对 VLC 的常见要求是具有“俯仰控制”机制。虽然应该有可能给 VLC 增加 LADPSA 俯仰控制,但还没有人着手去做。但是你仍然可以通过jack-rack添加 LADSPA 效果。

步骤如下:

  1. 您可能需要暂时停止 PulseAudio,例如使用pasuspender cat

  2. 使用以下命令启动照常运行的 Jack 守护进程:

    jackd -d alsa
    
    
  3. 开始qjackctl这样你就可以控制一些插孔连接。

  4. Start jack-rack. Using the + button, select Frequency ➤ Pitch shifters ➤ TAP Pitch Shifter. Don’t forget to enable it; it should look like Figure 29-5.

    A435426_1_En_29_Fig5_HTML.jpg

    图 29-5。

    Jack Rack with pitch shifter Note that in qjackctl, jack-rack shows as jack_rack (the minus has been replaced with an underscore), which is the proper Jack name of jack-rack. Connect the output of jack-rack to system.  

  5. Now start vlc --aout jack so you can set up the correct configuration. Choose Tools ➤ Preferences, and in “Show settings” set the radio button to All. Then under Audio ➤ Output modules ➤ Jack, check “Automatically connect to writable clients” and connect to clients matching jack_rack (note the underscore). This should look like Figure 29-6.

    A435426_1_En_29_Fig6_HTML.jpg

    图 29-6。

    VLC selecting output client  

  6. The next time you start vlc with, for example, vlc --aout jack BST.mp3, qjackctl should look like Figure 29-7.

    A435426_1_En_29_Fig7_HTML.jpg

    图 29-7。

    qjackctl with VLC connected to Jack Rack  

音乐应该通过jack-rack播放,你可以调整音高。

图 29-8 显示了 VLC 通过音调滤波器播放 MP3 音频的结果,同时也显示了 CDG 的视频。

A435426_1_En_29_Fig8_HTML.jpg

图 29-8。

VLC playing through pitch shifter

结论

本章讨论了构建插孔管道以给 MIDI 和 MP3+G 文件添加效果的多种方法。

三十、流式音频

流式音频通常涉及将音频从网络上的一个节点发送到另一个节点。有许多方法可以做到这一点,使用许多格式。本章简要讨论了其中的一些。

超文本传送协议

HTTP 是网络的基础协议。该协议不知道它承载的内容。虽然它最初是为传输 HTML 文档而设计的,但现在它被用来传输图像文件、Postscript 文档、PowerPoint 文件和几乎任何其他文件。这包括媒体文件,这本书的主题。

超文本传输协议服务器

内容通过 HTTP 服务器从网站传送。其中最著名的是 Apache,但是在 Linux 世界中,Nginx 和 Lighttpd 也很常见。还有许多专有服务器。

HTTP 服务器可以传送存储在服务器上的静态文件,也可以从数据库连接动态构建内容。

HTTP 客户端

HTTP 流有许多客户端,通常称为用户代理。这些包括浏览器以及前面讨论的许多音频播放器。

HTTP 浏览器

将你的浏览器指向一个音频文件的 URL,它会将内容传递给一个助手,该助手将尝试播放该文件。浏览器将基于 URL 的文件扩展名或者基于从 HTTP 服务器的 HTTP 头中传送的文件的内容类型来选择助手。

MPlayer

MPlayer 支持 HTTP。你只需给出文件的 URL。

mplayer http://localhost/audio/enigma/audio_01.ogg

可见光通讯

VLC 也知道 HTTP。你只需给出文件的 URL。

vlc http://localhost/audio/enigma/audio_01.ogg

流媒体与下载

如果你从网上下载一个文件,那么你可以在下载完成后播放它。这意味着播放被延迟,直到整个文件被保存到本地文件系统。由于它现在是本地的,所以它可以玩而不用担心网络延迟。下面是一个简单的 shell 脚本来说明这一点:

wget -O tmp  http://localhost/audio/enigma/audio_01.ogg
mplayer tmp
rm tmp

另一种方法是从网上读取资源,并使用某种管道将收到的资源传递给播放器。只要管道足够大,能够缓冲足够的资源,以应对网络延迟,这就没问题。下面举例说明:

wget -O -  http://localhost/audio/enigma/audio_01.ogg | mplayer -

(是的,我知道,MPlayer 可以直接流 URLs 我只是想说明一点。)

HTML5

HTML5 是 HTML 的最新版本。HTML5 是一个“生活标准”。啊!这意味着它根本不是一个标准,而只是一个处于不断变化状态的规范的标签。现在有一个音频元素<audio>,被很多浏览器实现。

例如,下面的 HTML 将首先尝试 Ogg 文件,如果客户端无法播放它,它将尝试 MP3 文件,如果无法播放,它将显示失败消息:

      <audio controls="controls"<
        <source src="audio_01.ogg" type="audio/ogg"<
          <source src="audio_01.mp3" type="audio/mpeg"<
            Your browser does not support the audio element.
      </audio<

图 30-1 显示了它在浏览器中的样子。

A435426_1_En_30_Fig1_HTML.jpg

图 30-1。

Caption

dlna!dlna!dlna

数字生活网络联盟(DLNA)旨在共享家庭网络中的数字媒体,如照片、音频和视频。它建立在通用即插即用(UPnP)协议套件之上。这反过来又建立在一个更丑陋的互联网标准 SOAP 之上。为了处理媒体信息,UPnP 本身使用了只能被描述为糟糕透顶的黑客技术,从而加剧了基础技术的糟糕选择。由于其最复杂的数据类型是字符串,UPnP 将完整的 XML 文档嵌入到这些字符串中,以便一个 XML 文档包含另一个 XML 文档作为嵌入字符串。哦,天哪,质量更好的工程师肯定能想出比这更好的解决方案!

UPnP 是开放的,因为它可以描述许多不同的家庭网络设备和数据格式。DLNA 将其限制在少数“认可的”类型,然后将该规范设为私有,只有在付费后才能使用。

尽管如此,越来越多的设备“支持 DLNA ”,如电视、蓝光播放器等。看来 DLNA 要在这里呆下去了。

马修·潘顿在《媒体流的 DLNA——这一切意味着什么( http://news.cnet.com/8301-17938_105-10007069-1.html )指出了 DLNA 的一些进一步的问题,主要涉及到支持的文件格式。我最近购买的一台索尼 BDP-S390 蓝光播放器说明了他的评论的真实性。根据需要支持 LPCM ( .wav),但在可选的 MP3、WMA9、AC-3、AAC、ATRAC3plus 中,仅支持 MP3、AAC/HE-AAC ( .m4a)和 WMA9 标准(.wma)。当然,奥格不在任何 DLNA 榜单上。

网站 DLNA 开源项目( http://elinux.org/DLNA_Open_Source_Projects )列出了一批 Linux DLNA 玩家。我已经成功地使用了 CyberGarage Java 客户端和服务器以及 MediaTomb 服务器。

冰铸

Shoutcast 是一款用于互联网音频流的专有服务器软件,它为流设置了标准。Icecast 是开源软件的有力竞争者,它和开源软件一样质量好,更优秀,并且支持更多的格式。对于流的接收者来说,Icecast 只是一个 HTTP 服务器。后端是有趣的部分,因为 Icecast 使用 Shoutcast 协议从各种来源接收音频,如在线广播、麦克风或播放列表。

IceS 是 Icecast 获取音频流的一种方式,包含在发行版中。更多信息,请参见 IceS v2.0 文档( www.icecast.org/docs/ices-2.0.2/ )。

流体运动

从 Flumotion 网站( www.flumotion.net/ ),“Flumotion 流媒体软件允许广播公司和公司在一台服务器上以所有领先的格式实时点播内容。Flumotion 还提供流媒体平台和网络电视,通过覆盖整个流媒体价值链来减少工作流程和成本。这种端到端的模块化解决方案包括信号采集、编码、多格式转码、内容流和一流的接口设计。媒体后台支持高级内容管理,并通过富媒体广告实现最佳盈利。”

结论

本章简要概述了一些可用的流机制。HTML5 嵌入提供了一种将音频(和视频)包含到网页中的简单方法,而 Icecast 和 Flumotion 等系统可以用于广播电台等专业音频系统。

三十一、树莓派

Raspberry Pi (RPi)是一台低成本的 Linux 计算机,开发的目的是为进入大学计算机科学课程的学生提供一个良好、廉价的游戏环境。确实如此。我有一群已经步入中年的同事,他们都迫不及待地想和我一起玩。不过,到目前为止,他们的孩子还没有看过....

资源

以下是一些资源:

基础知识

以下部分涵盖了基础知识。

五金器具

Raspberry Pi (RPi) 3 型号 B 有 1Gb RAM,四个 USB 端口,WiFi 和蓝牙,以及一个以太网端口。它具有 HDMI 和模拟音频和视频输出。以下来自 FAQ ( www.raspberrypi.org/faqs ):

"Except for the 2B/3B raspberry pie, Broadcom BCM2835 is used in all versions and revisions of raspberry pie. It consists of a floating-point ARM1176JZFS running at 700MHz and a VideoCore 4 GPU. The GPU can play blue light with H.264 at a speed of 40 megabits per second. It has a fast 3D kernel, which can be accessed through OpenGL ES2.0 and OpenVG libraries provided. The model used in 2B is Broadcom BCM2836. It consists of a quad-core ARM Cortex-a7 processor with floating point and NEON, running at 900MHz, and the same VideoCore 4 GPU as other models of Raspberry Pi. The model 3B uses Broadcom BCM2837, which includes a quad-core ARM Cortex-A53 running at 1.2GHz. Its GPU capability is equivalent to Pi 2. "

RPi 通过 HDMI 端口和模拟 3.5 毫米音频输出端口提供音频输出。中没有音频。但是,有 USB 端口,可以插入 USB 声卡,这是 Linux 发行版可以识别的。

CPU 是 ARM CPU。您可以在“ARM 与 x86 处理器:有何不同?”中找到 ARM 和 Intel 指令集之间的差异的简单概述( www.brighthub.com/computing/hardware/articles/107133.aspx )。

替代单板计算机

单板电脑很多。维基百科有单板电脑列表(http://en.wikipedia.org/wiki/List_of_single_board_computers);它们都是 RPi 的潜在替代品。这里只是一个快速选择:

Gumstix ( http://en.wikipedia.org/wiki/Gumstix

  • 这是一台存在了很多年的单板电脑(我在 2004 年得到了一台)。它的功率不高,但也不应该如此。

Arduino ( http://en.wikipedia.org/wiki/Arduino

  • Arduino 是为电子项目设计的微控制器。它使用 ARM Cortec-M3 CPU,比 RPi 的规格还要低。

Udo(www.udoo.org/

  • UDOO 试图将 RPi 和 Arduino 的精华与两个 CPU 结合到一台单板计算机中。

ODroid ( http://odroid.com/ )

BeagleBone ( http://beagleboard.org/Products/BeagleBone%20Black

  • BeagleBone Black 的 CPU (ARM Cortex-A8)略好于 RPi,但价格稍贵。

分散注意力

Raspberry Pi 站点提供了几个 Linux 映像,其他映像正在其他地方开发。我使用的是基于 Debian 的镜像,它有两种形式:使用 Debian 的软浮点和使用 FPU 的硬浮点,称为 Raspbian。体面的声音处理需要硬浮点图像,这在很大程度上取决于浮点。在 www.memetic.org/raspbian-benchmarking-armel-vs-armhf/ 有一篇很好的对标文章。另一组基准在 http://elinux.org/RaspberryPiPerformance 。基本上,这些表明,如果您想要良好的浮点性能,应该使用硬浮点版本,这是音频处理所必需的。

ELinux.org 维护着一个 RPi 分布列表( http://elinux.org/RPi_Distributions )。这里包含了许多标准的 Linux 发行版,比如 Fedora、Debian、Arch、SUSE、Gentoo 等等。RPi 作为一个基于 XBMC 媒体中心的媒体中心获得了广泛的关注,它的代表是 Raspbmc 和 OpenElec 等发行版。

那么,它和目前讨论的各种音频工具相处的怎么样呢?这是一个混合的包。

没有声音

我用 HDMI 接口把我的电脑插入 29 英寸的优派显示器。最初,3.5 毫米模拟输出或 HDMI 显示器都没有声音。这在“为什么我的音频(声音)输出不起作用?”( http://raspberrypi.stackexchange.com/questions/44/why-is-my-audio-sound-output-not-working )。我编辑了文件/boot/config.txt,取消了对行"hdmi_drive=2"的注释。我还使用了下面的命令,其中n0 =自动,1 =耳机,2=hdmi 来切换输出:

sudo amixer cset numid=3 <n>

之后声音就没问题了。

驱动

Raspberry Pi 使用 ALSA 驱动snd_bcm2835,这可以管理 HDMI 输出。命令alsa-info不存在,但是因为这是一个 shell 脚本,所以它可以从其他地方复制并在 RPi 上运行。大型发行版中的一些常用配置文件和命令丢失了,但是它在 RPi2 的 Raspbian 发行版中显示出来(有许多遗漏)。

!!################################
!!ALSA Information Script v 0.4.64
!!################################

!!Script ran on: Sun Nov 13 11:13:36 UTC 2016

!!ALSA Version
!!------------

Driver version:     k4.7.2-v7+
Library version:    1.0.28
Utilities version:  1.0.28

!!Loaded ALSA modules
!!-------------------

snd_bcm2835

!!Soundcards recognised by ALSA
!!-----------------------------

 0 [ALSA           ]: bcm2835 - bcm2835 ALSA
                      bcm2835 ALSA

!!Aplay/Arecord output
!!--------------------

APLAY

**** List of PLAYBACK Hardware Devices ****
card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
  Subdevices: 8/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
card 0: ALSA [bcm2835 ALSA], device 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

ARECORD

**** List of CAPTURE Hardware Devices ****

!!Amixer output
!!-------------

!!-------Mixer controls for card 0 [ALSA]

Card hw:0 'ALSA'/'bcm2835 ALSA'
  Mixer name    : 'Broadcom Mixer'
  Components    : ''
  Controls      : 6
  Simple ctrls  : 1
Simple mixer control 'PCM',0
  Capabilities: pvolume pvolume-joined pswitch pswitch-joined
  Playback channels: Mono
  Limits: Playback -10239 - 400
  Mono: Playback -2000 [77%] [-20.00dB] [on]

音频播放器样本

前面的章节已经广泛使用了一些音频工具。RPi 仍然是一个 Linux 系统,所以您会期望音频工具在 RPi 上表现正常。但是值得确认!

MPlayer

MPlayer 在默认的 ALSA 模块上可以很好地播放 MP3、OGG 和 WAV 文件。

可见光通讯

VLC 试图播放 WAV 文件,但它在软浮动发行版上被打断了。CPU 使用率上升了 90%左右,这是非常不可玩的。由于这个原因,这个软件发行版已经不再使用了。硬浮动发行版可以播放 MP3,OGG 和 WAV 文件。

alsaplayer

该程序使用标准的硬浮动发行版播放 Ogg-Vorbis 和 MP3 格式的文件。

omxplayer

RPi 有一个 GPU,这个可以被omxplayer使用。它可以播放 Ogg-Vorbis 文件,只占用 12%的 CPU,看起来是音频和视频的良好候选。

是 X 在用 CPU 吗?

显然不只是 X: gnome-player 工作正常。

采样音频采集

RPi 没有音频输入或线路输入端口。我通过通电的 USB 集线器连接声霸卡。它与arecord -l一起显示如下:

**** List of CAPTURE Hardware Devices ****
card 1: Pro [SB X-Fi Surround 5.1 Pro], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

所以,对 ALSA 来说,它是装置hw:1,0

驱动

如果你得到正确的选项,标准程序arecord就会工作。

arecord -D hw:1,0 -f S16_LE -c 2 -r 48000 tmp.s16
Recording WAVE 'tmp.s16' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo

生成的文件可以通过以下方式播放:

aplay -D hw:1,1 -c 2 -r 48000 -f S16_LE tmp.s16

在第十章,我给出了一个名为alsa_capture.c的程序的源代码。当运行以下程序时:

alsa_capture hw:1,0 tmp.s16

它以 48,000Hz 的频率记录立体声 PCM 数据。

MIDI 播放器

虽然标准音频工具工作正常,但 MIDI 播放器的 CPU 负担很重。这一节着眼于自定义普通玩家玩 OK。

TiMidity

RPi 2 上 TiMidity 的 CPU 平均占 50 %, RPi 3 上 TiMidity 的 CPU 平均占 38%。如果其他应用(如 GUI 前端)也在使用,这可能会使它不可用。

为了提高 RPi 的可用性,在timidity.cfg文件中,取消对以下行的注释:

## If you have a slow CPU, uncomment these:
#opt EFresamp=d         #disable resampling
#opt EFvlpf=d           #disable VLPF
#opt EFreverb=d         #disable reverb
#opt EFchorus=d         #disable chorus
#opt EFdelay=d          #disable delay
#opt anti-alias=d       #disable sample anti-aliasing
#opt EWPVSETOZ          #disable all Midi Controls
#opt p32a               #default to 32 voices with auto reduction
#opt s32kHz             #default sample frequency to 32kHz
#opt fast-decay         #fast decay notes

这使得 RP2 上的 CPU 使用率下降到大约 30%。(感谢骑士精神不才, http://chivalrytimberz.wordpress.com/2012/12/03/pi-lights/ )。)

皮卡拉奥克

这仅使用了 40%的 CPU,即使使用 GUI 也运行良好。

流体合成/qssynth

在 RPi2 和 RPi3 上,CPU 使用率上升了大约 85%到 90%。

行程安排

有时 FluidSynth 会抱怨无法重置调度程序。Aere Greenway (

aere - rtprio 85
aere - memlock unlimited

确保用您的用户 ID 代替aere

非原因

以下被认为是问题的原因,但最终被忽略:

  • FluidSynth 可以配置为使用 doubles 或 floats。默认是双精度,这些在 ARM 芯片上很慢。切换到浮点并没有消除 CPU 使用中的问题峰值。
  • FluidSynth 使用声音字体文件,这些文件相当大。通常约为 40MB。切换到较小的字体没有帮助;内存使用不是问题。
  • FluidSynth 中的缓冲很小。可以使用-z参数使其变大。缓冲不是问题,改变它也没有帮助。
  • 众所周知,许多操作在 CPU 中开销很大。FluidSynth 支持许多插值算法,可以使用命令解释器设置这些算法,例如使用interp 0关闭插值。其他昂贵的操作包括混响、复调和合唱。孤立地摆弄这些东西被证明是徒劳的。

解决方法

我发现的两个解决方案是

  • polyphony=64reverb=false
  • rate=22050,这将 CPU 使用率降低到大约 55%

Java 声音

我安装了 OpenJDK 版本 8,这是目前默认的 Java 版本。第十章给出了程序DeviceInfo。RPi 上的输出如下:

Mixers:
   PulseAudio Mixer, version 0.02
    Mixer: org.classpath.icedtea.pulseaudio.PulseAudioMixer@144bcfa
      Source lines
        interface SourceDataLine supporting 42 audio formats, and buffers of 0 to 1000000 bytes
        interface Clip supporting 42 audio formats, and buffers of 0 to 1000000 bytes
      Target lines
        interface TargetDataLine supporting 42 audio formats, and buffers of 0 to 1000000 bytes
   ALSA [default], version 4.7.2-v7+
    Mixer: com.sun.media.sound.DirectAudioDevice@d3c617
      Source lines
        interface SourceDataLine supporting 84 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 84 audio formats, and buffers of at least 32 bytes
      Target lines
   ALSA [plughw:0,0], version 4.7.2-v7+
    Mixer: com.sun.media.sound.DirectAudioDevice@1c63996
      Source lines
        interface SourceDataLine supporting 8 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 8 audio formats, and buffers of at least 32 bytes
      Target lines
   ALSA [plughw:0,1], version 4.7.2-v7+
    Mixer: com.sun.media.sound.DirectAudioDevice@11210ee
      Source lines
        interface SourceDataLine supporting 8 audio formats, and buffers of at least 32 bytes
        interface Clip supporting 8 audio formats, and buffers of at least 32 bytes
      Target lines
   Port ALSA [hw:0], version 4.7.2-v7+
    Mixer: com.sun.media.sound.PortMixer@40e464
      Source lines
      Target lines
        PCM target port

虽然这是使用 PulseAudio 混音器,但 PulseAudio 实际上并没有运行(在这个阶段)!所以,它只能使用 ALSA 接口。

第九章给出了程序PlayAudioFile。这个可以播放.wav文件 OK。但它不能播放 Ogg-Vorbis 或 MP3 文件,并会抛出一个UnsupportedAudioFileException

脉冲二极管(PulseAudio)

PulseAudio 从存储库中安装 OK,运行时没有任何问题。pulsedevlist的输出如下:

=======[ Output Device #1 ]=======
Description: bcm2835 ALSA Analog Stereo
Name: alsa_output.platform-bcm2835_AUD0.0.analog-stereo
Index: 0

=======[ Input Device #1 ]=======
Description: Monitor of bcm2835 ALSA Analog Stereo
Name: alsa_output.platform-bcm2835_AUD0.0.analog-stereo.monitor
Index: 0

Java MIDI 文件

openJDK 支持 Java MIDI 设备。程序DeviceInfo报告如下:

MIDI devices:
    Name: Gervill, Decription: Software MIDI Synthesizer, Vendor: OpenJDK
        Device is a synthesizer
        Open receivers:

        Default receiver: com.sun.media.sound.SoftReceiver@10655dd

        Open receivers now:
            com.sun.media.sound.SoftReceiver@10655dd

        Open transmitters:
        No default transmitter
    Name: Real Time Sequencer, Decription: Software sequencer, Vendor: Sun Microsystems
        Device is a sequencer
        Open receivers:

        Default receiver: com.sun.media.sound.RealTimeSequencer$SequencerReceiver@12f0999

        Open receivers now:
            com.sun.media.sound.RealTimeSequencer$SequencerReceiver@12f0999

        Open transmitters:

        Default transmitter: com.sun.media.sound.RealTimeSequencer$SequencerTransmitter@65a77f

        Open transmitters now:
            com.sun.media.sound.RealTimeSequencer$SequencerTransmitter@65a77f
Default system sequencer is Real Time Sequencer
Default system synthesizer is Gervill

像 DumpSequence 这样的程序工作正常。但是 SimpleMidiPlayer 达到了 80%的 CPU 使用率,是不可用的。因此,任何 Karaoke 播放器在 RPi 上使用 Java 的想法都是不好的。树莓派网站上有一个讨论声音问题的帖子( www.raspberrypi.org/phpBB3/viewtopic.php?f=38&t=11009 )。

最大 OpenMAX

可以使用 OpenMAX IL 工具包在 Raspberry Pi 上播放音频和视频。这是 Broadcom 为 RPi 使用的 GPU 实现的。这在第十三章中有部分介绍,在我的书《Raspberry Pi GPU 音频和视频编程》中有深入介绍。

结论

树莓派是一个令人兴奋的新玩具。这个市场上有许多竞争对手,但它仍然卖出了一千多万台设备。本章讲述了设备的一些音频方面。

三十二、总结

这是我最后的话。

我从哪里开始?

以下是这一切的起点:

  • 我有两台 Karaoke 机,各有不同的功能。
  • 我想用我的电脑建造一台“两全其美”的机器。
  • 我第一次尝试使用 Java Sound API 时工作正常,但是受到了延迟的影响,足以使它无法使用。
  • 试图将这个 Java 解决方案的任何部分移植到像 Raspberry Pi 这样的低功耗设备上的尝试都以惨败告终。

我到哪里了?

嗯,我已经走了大部分路了。我现在有一个系统在后台播放视频,并使用 TiMidity 合成器播放 Karaoke 文件。我没有让评分系统工作,但这涉及到对数字信号处理的进一步探索。

我实际上在树莓 Pi 上完成了所有工作,但这意味着深入研究树莓 Pi 的 GPU 来处理视频效果,我已经在另一本书中处理了它的 GPU 编程。

我是怎么到那里的?

很明显,我需要用声音来演奏。我从 Java 声音 API 开始,当它被证明有延迟问题时,我开始在 Linux 上搜索声音的所有方面。这就是为什么这本书有 ALSA、Jack、PulseAudio 等章节。我无法以足够清晰的方式找到我要找的信息,所以随着我发现的越来越多,我把它们都写了下来,结果就是这本书。

我希望你会发现它有普遍的价值,而不仅仅是让我特别着迷的东西。我在写这本书的过程中学到了很多,我相信如果你想在 Linux 下做任何关于声音的事情,这本书至少会给你一些答案。

问候,并祝你自己的项目好运!

三十三、解码 Sonken 卡拉 DVD 上的 DKD 文件

这一章是关于从我的 Sonken 卡拉 DVD 中获取信息,这样我就可以开始编写播放歌曲的程序。在 Linux 下不直接参与播放声音,可以跳过。

介绍

我有两台 Karaoke 机,一台 Sonken MD-388,一台 Malata MDVD-6619。在他们两个之间,他们拥有我认为我需要的 Karaoke 播放器的所有特征,包括以下:

  • 选曲和放曲子(当然!)
  • 大量的中文和英文歌曲(我妻子是中国人,我是英国人)
  • 中文歌曲有普通话和拼音,所以我也可以跟着唱
  • 旋律的音符与歌手实际唱的音符一起显示
  • 显示不同特征的评分系统

Malata 真的很好,因为它显示了旋律的音符,也显示了你正在唱的音符。但它的英文歌曲少得可怜,而且没有显示中文歌曲的拼音。Songen 在这两方面都有很好的选择,可以显示拼音,但不显示音符,并且有一个简单的评分系统。

所以,我想把歌曲从我的 Sonken DVD 上拿下来,在万利达或我的电脑上播放。在我的个人电脑上玩它们是首选,因为这样我就只受我能写的程序的限制,而不那么依赖于供应商的机器。所以,我的近期目标是把歌曲从 Sonken 的 DVD 上拿下来,开始用我想要的方式播放。

Sonken DVD 上的文件是 DKD 格式的。这是一种未记录的格式,可能代表数字 Karaoke 光盘。很多人都致力于这种格式,在卡拉 Engineering 等论坛上也有很多讨论。其中包括“了解加州电子 DVD 上的热狗文件”( http://old.nabble.com/Understanding-the-HOTDOG-files-on-DVD-of-California-electronics-td11359745.html ),“解码 JBK 6628 DVD Karaoke 碟片”( http://old.nabble.com/Decoding-JBK-6628-DVD-Karaoke-Disc-td12261269.html )(不过这两个环节似乎不再有任何内容),以及“Karaoke Huyndai 99”(http://board.midibuddy.net/showpost.php?p=533722&postcount=31)。

当我开始看我的光盘时,我的方向与这些论坛上的许多海报不同。此外,正如所预料的那样,论坛的结果是以一种临时的、常常令人困惑的方式提出的。所以,我最终重新发明了很多已经被发现的东西,也想出了一些新的东西。

事后看来,如果我对论坛上的言论给予足够的重视,我本可以节省数周的工作时间。因此,这个附录是我试图以一种简单而有逻辑的方式展示结果,以便试图用自己的光盘做类似事情的人可以很容易地找出什么适用于他们的情况,什么是不同的。

本章将涵盖以下内容:

  • 我的 DVD 上有什么文件
  • 每个文件包含的内容(概述)
  • 将歌曲标题与歌曲编号相匹配
  • 查找光盘上的歌曲数据
  • 提取歌曲数据
  • 解码歌曲数据

这个附录并不完整,因为还有更多有待发现。

格式转换

拷贝你的 DVD 不是违法的吗?它不在澳洲,在适当的条件下(见 www.ag.gov.au/Copyright/Issuesandreviews/Pages/CopyrightAmendmentAct2006FAQs.aspx 《版权修正法案 2006》常见问题)。

  • 我能把我的音乐收藏复制到我的 iPod 上吗?是的。您可以将自己的音乐格式转换到 MP3 播放器、Xbox 360 或电脑等设备上。

我只是把我合法购买的 Sonken DVD 上的音乐拷贝到我的电脑上供个人使用。这是在澳大利亚版权修正法案的范围内。你应该检查你的国家是否允许同样的权利。不要从我的 DVD 上索取任何文件的副本。那是违法的,我不会这么做的。

DVD 上的文件

我的 Sonken DVD 包含这些文件:

          BACK01.MPG
          DTSMUS00.DKD
          DTSMUS01.DKD
          DTSMUS02.DKD
          DTSMUS03.DKD
          DTSMUS04.DKD
          DTSMUS05.DKD
          DTSMUS06.DKD
          DTSMUS07.DKD
          DTSMUS10.DKD
          DTSMUS20.DKD

BACK01。每加仑行驶英里数

这是在后台播放的 MP3 文件。

dtsmus 00 . dkd 至 dtsmus 07 . dkd

这些是歌曲文件。这些的数量取决于 DVD 上有多少首歌曲。

dtsmus 10 . dkd

还没有人知道这个文件是干什么用的。

dtsmus 20 . dkd

该文件包含歌曲编号、歌曲标题和艺术家的列表,如歌曲书中所给。这个文件里的歌曲号比书里的歌曲号少一个。

解码 dtsmus 20 . dkd

我在 Linux 系统上,使用 Linux/Unix 实用程序和应用。Windows 和 Apple 等其他操作系统也有类似的版本。

歌曲信息

Unix 命令strings列出了一个文件中所有长度至少为四个字符的 ASCII 8 位编码字符串。在所有的 DVD 文件上运行这个命令会显示出DTSMUS20.DKD是唯一一个有很多英语字符串的文件,而这些字符串就是 DVD 上的歌曲标题。

简单的选择如下:

          Come To Me
          Come To Me Boy
          Condition Of My Heart
          Fly To The Sky
          Cool Love
          Count Down
          Cowboy
          Crazy

当然,光盘上显示的实际字符串取决于光盘上的歌曲。当然,你需要一些英文标题才能让它工作!

为了取得进一步的进展,您需要一个二进制编辑器。我使用的bvi. emacs也有二进制编辑模式。使用编辑器搜索光盘上已知的歌曲标题。例如,搜索甲壳虫乐队的“太阳来了”,会显示以下区块:

          000AA920  12 D3 88 48 65 72 65 20 43 6F 6D 65 73 20 54 68 ...Here Comes Th
          000AA930  65 20 52 61 69 6E 20 41 67 61 69 6E 00 45 75 72 e Rain Again.Eur
          000AA940  79 74 68 6D 69 63 73 00 1F 12 D3 89 48 65 72 65 ythmics.....Here
          000AA950  20 43 6F 6D 65 73 20 54 68 65 20 53 75 6E 00 42  Comes The Sun.B
          000AA960  65 61 74 6C 65 73 00 1B 12 D3 8A 48 65 72 65 20 eatles.....Here
          000AA970  46 6F 72 20 59 6F 75 00 46 69 72 65 68 6F 75 73 For You.Firehous

字符串“太阳来了”从 0xAA94C 开始,后跟一个空字节。接下来是以零结尾的“Beatles”紧接在这之前的是 4 个字节。这两个字符串(包括空字节)和 4 个字节的长度是 0x1F,这是前面四个字节的第一个。因此,该块由一个 4 字节的头、一个以空结尾的歌曲标题和一个以空结尾的艺术家组成。字节 1 是包括 4 字节标题的歌曲信息块的长度。

标题块的字节 2 是 0x12。jim75 在“解码 JBK 6628 DVD Karaoke 碟片”( http://old.nabble.com/Decoding-JBK-6628-DVD-Karaoke-Disc-td12261269.html )发现了文件JBK_Manual%5B1%5D.doc。其中有一个国家代码列表,如下所示:

          00 : KOREAN
          01 : CHINESE( reserved )
          02 : CHINESE
          03 : TAIWANESE
          04 : JAPANESE
          05 : RUSSIAN
          06 : THAI
          07 : TAIWANESE( reserved )
          08 : CHINESE( reserved )
          09 : CANTONESE
          12 : ENGLISH
          13 : VIETNAMESE
          14 : PHILIPPINE
          15 : TURKEY
          16 : SPANISH
          17 : INDONESIAN
          18 : MALAYSIAN
          19 : PORTUGUESE
          20 : FRENCH
          21 : INDIAN
          22 : BRASIL

甲壳虫乐队的歌曲在头的字节 2 中有 0x12,这与表中的国家代码相匹配。通过查看其他语言文件可以确认这一点。

我后来发现 WMA 的档案有他们自己的密码。到目前为止,我看到了以下内容:

          83 : CHINESE WMA
          92 : ENGLISH WMA
          94 : PHILIPPINE WMA

我想你可以从早期的照片中看出一种模式!

头的字节 3 和 4 是 0xD389,十进制是 54153。这比书里的歌号(54154)少了一个。所以,字节 3 和 4 是一个 16 位的短整数,比书中的歌曲索引少 1。

这种模式在整个文件中重复出现,所以每个记录都是这种格式。

数据的开始/结束

文件开头附近有一长串字节:“01 01 01 01 ....”这在我的文件 0x9F23 处结束。通过比较索引号和我的歌曲簿中的索引号,我确认这是韩国歌曲的开始,也可能是所有歌曲的开始。我还没有找到任何表给我这个起始值。

检查了一些歌曲后,我得到了这个表格:

  • 英文歌曲从 60x9562D 开始(歌曲 24452,类型 0x12)
  • 粤语 0x8F5D2(宋 13701,3 型)
  • 朝鲜语 at 0x9F23(歌曲 37847,类型 0)
  • 印度尼西亚文 at 0x11F942(宋 42002,类型 0x17)
  • 位于 0x134227 的印地语(歌曲 45058,类型 0x21)
  • 菲律宾 at 0xD5D20(宋 62775,类型 0x14)
  • 俄语 at 0x110428(歌曲 41012,类型 5)
  • 0xF5145 处的西班牙语(歌曲 26487,类型 0x16)
  • 0x413BE 处的普通话(1 个字符)(宋 1388,类型 3)

不过,我找不到越南歌曲。我的光盘上好像没有。我的歌本在说谎!我猜在某个地方有一些表格给出了这些起点,但是我还没有找到。这些都是看我的歌本然后在档案里找到的。

0x136C92 上的“FF FF FF FF …”序列表示模块结束。

但是在歌曲信息块之前和之后都有很多东西。我不知道这是什么意思。

中国歌曲

我书里的第一首英文歌是艾尔·维德的《阿甘正传》,歌曲编号 24452。在目录文件DTSMUS20.DK中,它位于 0x9562D (611885)。在此之前的条目是“20 03 3A 04 CE D2 B4 F2 C1 CB D2 BB CD A8 B2 BB CB B5 BB B0 B5 C4 B5 E7 BB B0 B8 F8 C4 E3 00 00。”歌曲代码是“3A 04”,换句话说就是 14852,也就是歌曲编号 14853(一个偏移量,记住!).当我在 Karaoke 机上播放这首歌时,我很幸运:这首歌的第一个字符是我,我把它认作是汉字“我”(拼音:wo3)。它在文件中的编码是“CE D2”我在电脑上安装了中文输入法,所以我可以搜索这个汉字。

Google 搜索 Unicode 值为我向我显示以下内容:

          [RESOLVED] Converting Unicode Character Literal to Uint16 variable ...
          www.codeguru.com › ... › C++ (Non Visual C++ Issues)
          5 posts - 2 authors - 1 Jul 2011

          I've determined that the unicode character '' has a hex value of
          0x6211 by looking it up on the "GNOME Character Map 2.32.1"
          and if I do this....

然后在 Unicode 搜索上查找 0x 6211(www.khngai.com/chinese/tools/codeunicode.php)给出黄金。

          Unicode       6211 (25105)
          GB Code       CED2 (4650)
          Big 5 Code    A7DA
          CNS Code      1-4A3C

第二行中的 CED2 是 GB 代码。因此,字符集是 GB(可能是 EUC-CN 编码的 GB2312 ),代码为我作为 CED2。

只是为了确定,使用玛丽·安塞尔在 GB 代码表( www.ansell-uebersetzungen.com/gborder.html )中的表格,将字节“CE D2 B4 F2 C1 CB D2 BB CD A8 B2 BB CB B5 BB B0 B5 C4 B5 E7 BB B0 B8 F8 C4 E3”翻译成我 打 了 一 通…,确实是这首歌。

其他语言

我不熟悉其他语言编码,所以没有研究过泰语、越南语等等。韩国人好像是 EUC 人。

程序

其他人的早期研究已经产生了 C 或 C++程序。这些通常是独立的程序。我想建立一个可重用模块的集合,所以我选择 Java 作为实现语言。

Java 美食

Java 是一种很好的面向对象的语言,支持良好的设计。它包括一个 MIDI 播放器和 MIDI 类。它支持多种语言编码,所以很容易从 GB-2312 转换到 Unicode。它具有良好的跨平台 GUI 支持。

java 伙伴

Java 不支持无符号整数类型。这真的很糟糕,因为对于这些程序来说,很多数据类型都是无符号的。Java 中的偶数字节是有符号的。这里有一些窍门:

  • 使所有类型的大小增加:byte 到 int,int 到 long,long 到 long。只希望不需要无符号长整型。

  • 如果你需要一个无符号字节,你有一个 int,你需要它适合 8 位,转换成一个字节,希望它不要太大。

  • 到处进行类型转换以使编译器满意,例如当需要从 int,(byte) n中取出一个字节时。

  • 注意到处都是标志。如果要右移一个数字,运算符>>会保留符号扩展名,因此,例如,在二进制 1XYZ…中,会移至 1111XYZ…您需要使用>>>,结果为 0001XYZ。

  • 如果你想把一个无符号的字节赋给一个 int,再看一下 signs。您可能需要以下内容:

                  n = b ≥ 0 ? b : 256 - b
    
    
  • 要从两个无符号字节构建一个无符号 int,符号会再次填充你:n = (b1 << 8) + b2 会出错,如果 b1 或 b2 是-ve。而是用下面的:

                  n = ((b1 ≥ 0 ? b1 : 256 - b1) << 8) + (b2 ≥ 0 ? b2 : 256 - b2)
    
    

    (不开玩笑!)

班级

歌曲类SongInformation.java包含关于一首歌曲的信息,如下所示:

public class SongInformation {

    // Public fields of each song record
    /**
     *  Song number in the file, one less than in songbook
     */
    public long number;

    /**
     * song title in Unicode
     */
    public String title;

    /**
     * artist in Unicode
     */
    public String artist;

    /**
     * integer value of language code
     */
    public int language;

    public static final int  KOREAN = 0;
    public static final int  CHINESE1 = 1;
    public static final int  CHINESE2 = 2;
    public static final int  TAIWANESE3 = 3 ;
    public static final int  JAPANESE = 4;
    public static final int  RUSSIAN = 5;
    public static final int  THAI = 6;
    public static final int  TAIWANESE7 = 7;
    public static final int  CHINESE8 = 8;
    public static final int  CANTONESE = 9;
    public static final int  ENGLISH = 0x12;
    public static final int  VIETNAMESE = 0x13;
    public static final int  PHILIPPINE = 0x14;
    public static final int  TURKEY = 0x15;
    public static final int  SPANISH = 0x16;
    public static final int  INDONESIAN = 0x17;
    public static final int  MALAYSIAN = 0x18;
    public static final int  PORTUGUESE = 0x19;
    public static final int  FRENCH = 0x20;
    public static final int  INDIAN = 0x21;
    public static final int  BRASIL = 0x22;
    public static final int  CHINESE131 = 131;
    public static final int  ENGLISH146 = 146;
    public static final int  PHILIPPINE148 = 148;

    public SongInformation(long number,
                           String title,
                           String artist,
                           int language) {
        this.number = number;
        this.title = title;
        this.artist = artist;
        this.language = language;
    }

    public String toString() {
        return "" + (number+1) + " (" + language + ") \"" + title + "\" " + artist;
    }

    public boolean titleMatch(String pattern) {
        // System.out.println("Pattern: " + pattern);
        return title.matches("(?i).*" + pattern + ".*");
    }

    public boolean artistMatch(String pattern) {
        return artist.matches("(?i).*" + pattern + ".*");
    }

    public boolean numberMatch(String pattern) {
        Long n;
        try {
            n = Long.parseLong(pattern) - 1;
            //System.out.println("Long is " + n);
        } catch(Exception e) {
            //System.out.println(e.toString());
            return false;
        }
        return number == n;
    }

    public boolean languageMatch(int lang) {
        return language == lang;
    }
}

歌曲表类SongTable.java保存了歌曲信息对象的列表。

import java.util.Vector;
import java.io.FileInputStream;
import java.io.*;
import java.nio.charset.Charset;

// public class SongTable implements java.util.Iterator {
// public class SongTable extends  Vector<SongInformation> {
public class SongTable {

    private static final String SONG_INFO_FILE = "/home/newmarch/Music/karaoke/sonken/DTSMUS20.DKD";
    private static final long INFO_START = 0x9F23;

    public static final int ENGLISH = 0x12;

    private static Vector<SongInformation> allSongs;

    private Vector<SongInformation> songs =
        new Vector<SongInformation>  ();

    public static long[] langCount = new long[0x23];

    public SongTable(Vector<SongInformation> songs) {
        this.songs = songs;
    }

    public SongTable() throws java.io.IOException,
                              java.io.FileNotFoundException {
        FileInputStream fstream = new FileInputStream(SONG_INFO_FILE);
        fstream.skip(INFO_START);
        while (true) {
            int len;
            int lang;
            long number;

            len = fstream.read();
            lang = fstream.read();
            number = readShort(fstream);
            if (len == 0xFF && lang == 0xFF && number == 0xFFFFL) {
                break;
            }
            byte[] bytes = new byte[len - 4];
            fstream.read(bytes);
            int endTitle;
            // find null at end of title
            for (endTitle = 0; bytes[endTitle] != 0; endTitle++)
                ;
            byte[] titleBytes = new byte[endTitle];
            byte[] artistBytes = new byte[len - endTitle - 6];

            System.arraycopy(bytes, 0, titleBytes, 0, titleBytes.length);
            System.arraycopy(bytes, endTitle + 1,
                             artistBytes, 0, artistBytes.length);
            String title = toUnicode(lang, titleBytes);
            String artist = toUnicode(lang, artistBytes);
            // System.out.printf("artist: %s, title: %s, lang: %d, number %d\n", artist, title, lang, number);
            SongInformation info = new SongInformation(number,
                                                       title,
                                                       artist,
                                                       lang);
            songs.add(info);

            if (lang > 0x22) {
                //System.out.println("Illegal lang value " + lang + " at song " + number);
            } else {
                langCount[lang]++;
            }
        }
        allSongs = songs;
    }

    public void dumpTable() {
        for (SongInformation song: songs) {
            System.out.println("" + (song.number+1) + " - " +
                               song.artist + " - " +
                               song.title);
        }
    }

    public java.util.Iterator<SongInformation> iterator() {
        return songs.iterator();
    }

    private int readShort(FileInputStream f)  throws java.io.IOException {
        int n1 = f.read();
        int n2 = f.read();
        return (n1 << 8) + n2;
    }

    private String toUnicode(int lang, byte[] bytes) {
        switch (lang) {
        case SongInformation.ENGLISH:
        case SongInformation.ENGLISH146:
        case SongInformation.PHILIPPINE:
        case SongInformation.PHILIPPINE148:
            // case SongInformation.HINDI:
        case SongInformation.INDONESIAN:
        case SongInformation.SPANISH:
            return new String(bytes);

        case SongInformation.CHINESE1:
        case SongInformation.CHINESE2:
        case SongInformation.CHINESE8:
        case SongInformation.CHINESE131:
        case SongInformation.TAIWANESE3:
        case SongInformation.TAIWANESE7:
        case SongInformation.CANTONESE:
            Charset charset = Charset.forName("gb2312");
            return new String(bytes, charset);

        case SongInformation.KOREAN:
            charset = Charset.forName("euckr");
            return new String(bytes, charset);

        default:
            return "";
        }
    }

    public SongInformation getNumber(long number) {
        for (SongInformation info: songs) {
            if (info.number == number) {
                return info;
            }
        }
        return null;
    }

    public SongTable titleMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.titleMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

     public SongTable artistMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.artistMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

      public SongTable numberMatches( String pattern) {
        Vector<SongInformation> matchSongs =
            new Vector<SongInformation>  ();

        for (SongInformation song: songs) {
            if (song.numberMatch(pattern)) {
                matchSongs.add(song);
            }
        }
        return new SongTable(matchSongs);
    }

    public String toString() {
        StringBuffer buf = new StringBuffer();
        for (SongInformation song: songs) {
            buf.append(song.toString() + "\n");
        }
        return buf.toString();
    }

    public static void main(String[] args) {
        // for testing
        SongTable songs = null;
        try {
            songs = new SongTable();
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }
        songs.dumpTable();
        System.exit(0);

        // Should print "54151 Help Yourself Tom Jones"
        System.out.println(songs.getNumber(54150).toString());

        // Should print "18062 伦巴(恋歌) 伦巴"
        System.out.println(songs.getNumber(18061).toString());

        System.out.println(songs.artistMatches("Tom Jones").toString());
        /* Prints
54151 Help Yourself Tom Jones
50213 Daughter Of Darkness Tom Jones
23914 DELILAH Tom Jones
52834 Funny Familiar Forgotten Feelings Tom Jones
54114 Green green grass of home Tom Jones
54151 Help Yourself Tom Jones
55365 I (WHO HAVE NOTHING) TOM JONES
52768 I Believe Tom Jones
55509 I WHO HAVE NOTHING TOM JONES
55594 I'll Never Fall Inlove Again Tom Jones
55609 I'm Coming Home Tom Jones
51435 It's Not Unusual Tom Jones
55817 KISS Tom Jones
52842 Little Green Apples Tom Jones
51439 Love Me Tonight Tom Jones
56212 My Elusive Dream TOM JONES
56386 ONE DAY SOON Tom Jones
22862 THAT WONDERFUL SOUND Tom Jones
57170 THE GREEN GREEN GRASS OF HOME TOM JONES
57294 The Wonderful Sound Tom Jones
23819 TILL Tom Jones
51759 What's New Pussycat Tom Jones
52862 With These Hands Tom Jones
57715 Without Love Tom Jones
57836 You're My World Tom Jones
        */

        for (int n = 1; n < langCount.length; n++) {
            if (langCount[n] != 0) {
                System.out.println("Count: " + langCount[n] + " of lang " + n);
            }
        }

        // Check Russian, etc
        System.out.println("Russian " + '\u0411');
        System.out.println("Korean " + '\u0411');
        System.exit(0);
    }
}

您可能需要调整基于文件的构造函数中的常量值,这样才能正常工作。

使用 Swing 来显示和搜索歌曲标题的 Java 程序是SongTableSwing.java

import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import javax.swing.SwingUtilities;
import java.util.regex.*;
import java.io.*;

public class SongTableSwing extends JPanel {
   private DefaultListModel model = new DefaultListModel();
    private JList list;
    private static SongTable allSongs;

    private JTextField numberField;
    private JTextField langField;
    private JTextField titleField;
    private JTextField artistField;

    // This font displays Asian and European characters.
    // It should be in your distro.
    // Fonts displaying all Unicode are zysong.ttf and Cyberbit.ttf
    // See http://unicode.org/resources/fonts.html
    private Font font = new Font("WenQuanYi Zen Hei", Font.PLAIN, 16);
    // font = new Font("Bitstream Cyberbit", Font.PLAIN, 16);

    private int findIndex = -1;

    /**
     * Describe <code>main</code> method here.
     *
     * @param args a <code>String</code> value
     */
    public static final void main(final String[] args) {
        allSongs = null;
        try {
            allSongs = new SongTable();
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }

        JFrame frame = new JFrame();
        frame.setTitle("Song Table");
        frame.setSize(1000, 800);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        SongTableSwing panel = new SongTableSwing(allSongs);
        frame.getContentPane().add(panel);

        frame.setVisible(true);
    }

    public SongTableSwing(SongTable songs) {

        if (font == null) {
            System.err.println("Can't fnd font");
        }

        int n = 0;
        java.util.Iterator<SongInformation> iter = songs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
            // model.add(n++, iter.next().toString());
        }

        BorderLayout mgr = new BorderLayout();

        list = new JList(model);
        // list = new JList(songs);
        list.setFont(font);
        JScrollPane scrollPane = new JScrollPane(list);

        setLayout(mgr);
        add(scrollPane, BorderLayout.CENTER);

        JPanel bottomPanel = new JPanel();
        bottomPanel.setLayout(new GridLayout(2, 1));
        add(bottomPanel, BorderLayout.SOUTH);

        JPanel searchPanel = new JPanel();
        bottomPanel.add(searchPanel);
        searchPanel.setLayout(new FlowLayout());

        JLabel numberLabel = new JLabel("Number");
        numberField = new JTextField(5);

        JLabel langLabel = new JLabel("Language");
        langField = new JTextField(8);

        JLabel titleLabel = new JLabel("Title");
        titleField = new JTextField(20);
        titleField.setFont(font);

        JLabel artistLabel = new JLabel("Artist");
        artistField = new JTextField(10);
        artistField.setFont(font);

        searchPanel.add(numberLabel);
        searchPanel.add(numberField);
        // searchPanel.add(langLabel);
        // searchPanel.add(langField);
        searchPanel.add(titleLabel);
        searchPanel.add(titleField);
        searchPanel.add(artistLabel);
        searchPanel.add(artistField);

        titleField.getDocument().addDocumentListener(new DocumentListener() {
                public void changedUpdate(DocumentEvent e) {
                    // rest find to -1 to restart any find searches
                    findIndex = -1;
                    // System.out.println("reset find index");
                }
                public void insertUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void removeUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset remove find index");
                }
            }
            );
        artistField.getDocument().addDocumentListener(new DocumentListener() {
                public void changedUpdate(DocumentEvent e) {
                    // rest find to -1 to restart any find searches
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void insertUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
                public void removeUpdate(DocumentEvent e) {
                    findIndex = -1;
                    // System.out.println("reset insert find index");
                }
            }
            );

        titleField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    filterSongs();
                }});
        artistField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    filterSongs();
                }});
        numberField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    filterSongs();
                }});

        JPanel buttonPanel = new JPanel();
        bottomPanel.add(buttonPanel);
        buttonPanel.setLayout(new FlowLayout());

        JButton find = new JButton("Find");
        JButton filter = new JButton("Filter");
        JButton reset = new JButton("Reset");
        JButton play = new JButton("Play");
        buttonPanel.add(find);
        buttonPanel.add(filter);
        buttonPanel.add(reset);
        buttonPanel.add(play);

        find.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    findSong();
                }
            });

        filter.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    filterSongs();
                }
            });

        reset.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    resetSongs();
                }
            });

        play.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    playSong();
                }
            });

     }

    public void findSong() {
        String number = numberField.getText();
        String language = langField.getText();
        String title = titleField.getText();
        String artist = artistField.getText();

        if (number.length() != 0) {
            try {

                long num = Integer.parseInt(number) - 1;
                for (int n = 0; n < model.getSize(); n++) {
                    SongInformation info = (SongInformation) model.getElementAt(n);
                    if (info.number == num) {
                        list.setSelectedIndex(n);
                        list.ensureIndexIsVisible(n);
                        return;
                    }
                }
            } catch(Exception e) {
                System.err.println("Not a number");
                numberField.setText("");
            }

            return;
        }

        /*
        System.out.println("Title " + title + title.length() +
                           "artist " + artist + artist.length() +
                           " find start " + findIndex +
                           " model size " + model.getSize());
        if (title.length() == 0 && artist.length() == 0) {
            System.err.println("no search terms");
            return;
        }
        */

        //System.out.println("Search " + searchStr + " from index " + findIndex);
        for (int n = findIndex + 1; n < model.getSize(); n++) {
            SongInformation info = (SongInformation) model.getElementAt(n);
            //System.out.println(info.toString());

            if ((title.length() != 0) && (artist.length() != 0)) {
                if (info.titleMatch(title) && info.artistMatch(artist)) {
                    // System.out.println("Found " + info.toString());
                        findIndex = n;
                        list.setSelectedIndex(n);
                        list.ensureIndexIsVisible(n);
                        break;
                }
            } else {
                if ((title.length() != 0) && info.titleMatch(title)) {
                    // System.out.println("Found " + info.toString());
                    findIndex = n;
                    list.setSelectedIndex(n);
                    list.ensureIndexIsVisible(n);
                    break;
                } else if ((artist.length() != 0) && info.artistMatch(artist)) {
                    // System.out.println("Found " + info.toString());
                    findIndex = n;
                    list.setSelectedIndex(n);
                    list.ensureIndexIsVisible(n);
                    break;

                }
            }

        }
    }

    public void filterSongs() {
        String title = titleField.getText();
        String artist = artistField.getText();
        String number = numberField.getText();
        SongTable filteredSongs = allSongs;

        if (allSongs == null) {
            // System.err.println("Songs is null");
            return;
        }

        if (title.length() != 0) {
            filteredSongs = filteredSongs.titleMatches(title);
        }
        if (artist.length() != 0) {
            filteredSongs = filteredSongs.artistMatches(artist);
        }
        if (number.length() != 0) {
            filteredSongs = filteredSongs.numberMatches(number);
        }

        model.clear();
        int n = 0;
        java.util.Iterator<SongInformation> iter = filteredSongs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
        }
    }

    public void resetSongs() {
        artistField.setText("");
        titleField.setText("");
        numberField.setText("");
        model.clear();
        int n = 0;
        java.util.Iterator<SongInformation> iter = allSongs.iterator();
        while(iter.hasNext()) {
            model.add(n++, iter.next());
        }
    }
    /**
     * "play" a song by printing its id to standard out.
     * Can be used in a pipeline this way
     */
    public void playSong() {
        SongInformation song = (SongInformation) list.getSelectedValue();
        if (song == null) {
            return;
        }
        long number = song.number + 1;

        System.out.println("" + number);
    }

    class SongInformationRenderer extends JLabel implements ListCellRenderer {

        public Component getListCellRendererComponent(
                                                      JList list,
                                                      Object value,
                                                      int index,
                                                      boolean isSelected,
                                                      boolean cellHasFocus) {
            setText(value.toString());
            return this;
        }
    }
}

当选择 Play 时,它会将歌曲 ID 打印到标准输出,以便在管道中使用。

数据文件

以下部分将介绍数据文件。

一般

文件DTSMUS00.DKDDTSMUS07.DKD包含音乐文件。音乐有两种格式:微软 WMA 文件和 MIDI 文件。在我的歌本里,有些歌是标注有歌手的。这些是 WMA 的档案。没有歌手的是 MIDI 文件。

WMA 的文件就是这样。MIDI 文件被略微压缩,在播放之前必须解码。

每个歌曲块在开头都有一个包含歌词的部分。这些是压缩的,必须解码。

一首歌曲的数据形成一个连续字节的记录。这些记录被收集成块,也是连续的。这两块是分开的。有一个指向这些块的指针“超级块”。歌曲号的一部分是超级块的索引,选择该块。歌曲号的其余部分是块中记录的索引。

我的路线

我回到了这一点,并在一段时间后才理解了其他人的成就。所以,为了帮助其他人,这是我的路线。

我用 Unix 命令strings发现了DTSMUS10.DKD中的歌曲信息。在其他文件中,它似乎没有产生太多。但是这些文件中有 ASCII 字符串,有些是重复的。所以,我写了一个 shell 管道来对这些字符串进行排序和计数。一个文件的管道如下:

          strings DTSMUS05.DKD | sort |uniq -c | sort -n -r |less

这产生了以下结果:

          1229 :^y|
          1018 j?wK
          843 ]/<
          756  Seh
          747  Ser
          747 _\D+P
          674 :^yt
          234 IRI$

结果并不令人鼓舞。但是,当我查看文件内部以查看“Ser”出现的位置时,我还看到了以下内容:

          q03C3E230  F6 01 00 00 00 02 00 16 00 57 00 69 00 6E 00 64 .........W.i.n.d
          03C3E240  00 6F 00 77 00 73 00 20 00 4D 00 65 00 64 00 69 .o.w.s. .M.e.d.i
          03C3E250  00 61 00 20 00 41 00 75 00 64 00 69 00 6F 00 20 .a. .A.u.d.i.o.
          03C3E260  00 39 00 00 00 24 00 20 00 34 00 38 00 20 00 6B .9...$. .4.8\. .k
          03C3E270  00 62 00 70 00 73 00 2C 00 20 00 34 00 34 00 20 .b.p.s.,. .4.4.
          03C3E280  00 6B 00 48 00 7A 00 2C 00 20 00 73 00 74 00 65 .k.H.z.,. .s.t.e
          03C3E290  00 72 00 65 00 6F 00 20 00 31 00 2D 00 70 00 61 .r.e.o. .1.-.p.a
          03C3E2A0  00 73 00 73 00 20 00 43 00 42 00 52 00 00 00 02 .s.s. .C.B.R....
          03C3E2B0  00 61 01 91 07 DC B7 B7 A9 CF 11 8E E6 00 C0 0C .a..............
          03C3E2C0  20 53 65 72 00 00 00 00 00 00 00 40 9E 69 F8 4D  Ser.......@.i.M

哇哦!双字节字符!

strings命令有选项可以查看,例如,2 字节的大端字符串。命令

          strings -e b DTSMUS05.DKD

发现了这个:

          IsVBR
          DeviceConformanceTemplate
          WM/WMADRCPeakReference
          WM/WMADRCAverageReference
          WMFSDKVersion
          9.00.00.2980
          WMFSDKNeeded
          0.0.0.0000

这些都是 WMA 格式的一部分。

根据加里·凯斯勒的文件签名表( www.garykessler.net/library/file_sigs.html ),WMA 文件的签名由下面所示的标题给出:

          30 26 B2 75 8E 66 CF 11
          A6 D9 00 AA 00 62 CE 6C

这种模式确实会出现,之前的字符串会在一段时间后出现。

ASF/WMA 文件格式的规范在 www.microsoft.com/download/en/details.aspx?displaylang=en&id=14995 ,尽管建议你不要阅读它,以防你想对这样的文件做任何开源的事情。

因此,在此基础上,我可以确定 WMA 文件的开始。每个 WMA 文件前面的四个字节是文件的长度。从那里我可以找到文件的结尾,它原来是下一个包含一些内容的记录的开始,然后是下一个 WMA 文件。

在这些记录中,我可以看到我无法理解的模式,但从字节 36 开始,我可以看到类似这样的字符串:

          AIN'T IT FUNNY HOW TIME SLIPS AWAY, Str length: 34

          00000000  10 50 41 10 50 49 10 50 4E 10 50 27 10 50 54 10 .PA.PI.PN.P'.PT.
          00000010  50 20 11 F1 25 12 71 05 04 61 05 05 51 21 13 01 P ..%.q..a..Q!..
          00000020  02 05 91 2B 10 20 48 10 50 4F 10 50 57 13 40 00 ...+. H.PO.PW.@.
          00000030  12 61 02 12 01 02 04 D1 05 04 51 3B 05 31 05 04 .a........Q;.1..
          00000040  C1 29 10 20 50 10 51 45 10 21 28 10 21 1E 10 21 .). P.QE.!(.!..!
          00000050  3A 14 F1 05 13 31 02 10 C1 0E 11 A1 58 15 A0 00 :....1......X...
          00000060  15 70 00 13 A0 A9                               .p....

能看到AIN'T(作为.PA.PI.PN.P'.PT)吗?

但是我不知道编码是什么,也不知道如何找到歌曲开始的表格。那时,我准备看看早期的东西,并了解它如何适用于我。(参见《了解加州电子 DVD 上的热狗文件》( http://old.nabble.com/Understanding-the-HOTDOG-files-on-DVD-of-California-electronics-td11359745.html )、《解码 JBK 6628 DVD Karaoke 碟》( http://old.nabble.com/Decoding-JBK-6628-DVD-Karaoke-Disc-td12261269.html )、《Karaoke Huyndai 99》(http://board.midibuddy.net/showpost.php?p=533722&postcount=31)。

超级街区

文件DTSMUS00.DKD以一串空值开始。在 0x200 处,它开始输入数据。这被认为是“表的表”的开始,换句话说,是一个超级块。这个超级块中的每个条目都是一个 4 字节的整数,它是数据文件中表的索引。超级块由一系列空值终止(对我来说是 0x5F4),表中的索引少于 256 个。

这些超级块条目的值似乎在不同的版本中发生了变化。在 JBK 光盘和我的光盘中,这些值必须乘以 0x800 才能在数据文件中给出一个“虚拟偏移量”。

为了说明这一点,在我的光盘 0x200 上有以下内容:

          00000200  00 00 00 01 00 00 08 6C 00 00 0F C1 00 00 17 7A
          00000210  00 00 1E 81 00 00 25 21 00 00 2B 8D 00 00 32 B7

因此,表值为 0x1、0x86C、0xFC1、0x177A、....“虚拟地址”是 0x800、0x436000 (0x86C * 0x800)等等。如果你去这些地址,你会看到地址前是一堆空值,在那个地址是数据。

我称它们为虚拟地址,因为在我的 DVD 上有八个数据文件,并且大多数地址比任何一个文件都大。我这里的文件(除了最后一个)都是 1065353216L 字节。“显而易见”的解决方案是可行的:文件号是地址/文件大小,文件中的偏移量是地址百分比文件大小。您可以通过查找每个块地址前的空值来检查这一点。

歌曲开始表

从超级块索引的每个表都是歌曲索引表。每个表包含 4 字节的索引。每个表最多有 0x100 个条目,或者以零索引结束。每个索引是从歌曲条目开始的表格开始的偏移。

从歌曲编号定位歌曲条目

给定一个歌曲编号,比如 54154,“太阳来了”,您现在可以找到歌曲条目。将歌曲编号减少 1 至 54153。它是一个 16 位的数字。最高 8 位是超级块中歌曲索引表的索引。底部的 8 位是歌曲索引表中歌曲条目的索引。

下面是伪代码:

          songNumber = get number for song from DTSMUS20.DKD
          superBlockIdx = songNumber >>
          indexTableIdx = songNumber & 0xFF

          seek(DTSMUS00.DKD, superBlockIdx)
          superBlockValue = read 4-byte int from DTSMUS00.DKD

          locationIndexTable = superBlockValue * 0x800
          fileNumber = locationIndexTable / fileSize
          indexTableStart = locationIndexTable % fileSize
          entryLocation = indexTableStart + indexTableIdx

          seek(fileNumber, entryLocation)
          read song entry

歌曲条目

每个歌曲条目都有一个标题,后面是两个块,我称之为信息块和歌曲数据块。每个标题块有一个 2 字节的类型码和一个 2 字节的整数长度。类型代码为 0x0800 或 0x0000。代码表示歌曲数据的编码:0x0800 是 WMA 文件,而 0x0000 是 MIDI 文件。

如果类型码是 0x0 比如披头士的“救命!”(歌曲号 51765),则信息块具有标题块中的长度,并从 12 个字节开始。歌曲数据块紧随其后。

如果类型代码是 0x8000,则信息块从 4 个字节开始,长度为报头中给定的长度。歌曲块从信息块末尾的下一个 16 字节边界开始。

歌曲块以一个 4 字节的头开始,这是所有类型的歌曲数据的长度。

歌曲数据

如果歌曲类型是 0x8000,则歌曲数据是 WMA 文件。所有歌曲都有一个歌手包含在这个文件中。

如果歌曲类型是 0x0,那么(从书中)在所查看的歌曲中没有歌手。该文件被编码和解码为 MIDI 文件。

解码 MIDI 文件

所有文件都有一个歌词块,后跟一个音乐块。歌词块被压缩,并且已经发现这是 LZW 压缩。这会解压缩成一组 4 字节的块。前两个字节是歌词的字符。对于 1 字节编码,如英语或越南语,第一个字节是一个字符,第二个字节是零或另一个字符(两个字节,如\r\n)。对于双字节编码,如 GB-2312,两个字节构成一个字符。

接下来的两个字节是字符串播放的时间长度。

歌词块

每个歌词块都以""#0001 @@00@12 @Help Yourself @ @@Tom Jones"这样的字符串开头。这里的语言代码是@00@NN中的NN。歌名,作词人,歌手都很清楚。(注意:这些字符都是相隔 4 个字节的!)对于英语,是 12 等等。

每个块的字节 0 和 1 是歌词中的一个字符。字节 2 和 3 是每个字符的持续时间。要将它们转换成 MIDI 数据,必须将持续时间转换成每个字符的开始/停止。

我做这个的 Java 程序是SongExtracter.java

import java.io.*;
import javax.sound.midi.*;
import java.nio.charset.Charset;

public class SongExtracter {
    private static final boolean DEBUG = false;

    private String[] dataFiles = new String[] {
        "DTSMUS00.DKD", "DTSMUS01.DKD", "DTSMUS02.DKD",
        "DTSMUS03.DKD", "DTSMUS04.DKD", "DTSMUS05.DKD",
        "DTSMUS06.DKD", "DTSMUS07.DKD"};
    private String superBlockFileName = dataFiles[0];
    private static final String DATADIR = "/home/newmarch/Music/karaoke/sonken/";
    private static final String SONGDIR ="/home/newmarch/Music/karaoke/sonken/songs/";
    //private static final String SONGDIR ="/server/KARAOKE/KARAOKE/Sonken/";
    private static final long SUPERBLOCK_OFFSET = 0x200;
    private static final long BLOCK_MULTIPLIER = 0x800;
    private static final long FILE_SIZE = 0x3F800000L;

    private static final int SIZE_UINT = 4;

    private static final int SIZE_USHORT = 2;

    private static final int ENGLISH = 12;

    public RawSong getRawSong(int songNumber)
        throws java.io.IOException,
               java.io.FileNotFoundException {
        if (songNumber < 1) {
            throw new FileNotFoundException();
        }

        // song number in files is one less than song number in books, so
        songNumber--;

        long locationIndexTable = getTableIndexFromSuperblock(songNumber);
        debug("Index table at %X\n", locationIndexTable);

        long locationSongDataBlock = getSongIndex(songNumber, locationIndexTable);

        // Now we are at the start of the data block
        return readRawSongData(locationSongDataBlock);

        //debug("Data block at %X\n", songStart);
    }

    private long getTableIndexFromSuperblock(int songNumber)
        throws java.io.IOException,
               java.io.FileNotFoundException {
        // index into superblock of table of song offsets
        int superBlockIdx = songNumber >> 8;

        debug("Superblock index %X\n", superBlockIdx);

        File superBlockFile = new File(DATADIR + superBlockFileName);

        FileInputStream fstream = new FileInputStream(superBlockFile);

        fstream.skip(SUPERBLOCK_OFFSET + superBlockIdx * SIZE_UINT);
        debug("Skipping to %X\n", SUPERBLOCK_OFFSET + superBlockIdx*4);
        long superBlockValue = readUInt(fstream);

        // virtual address of the index table for this song
        long locationIndexTable = superBlockValue * BLOCK_MULTIPLIER;

        return locationIndexTable;
    }

    /*
     * Virtual address of song data block
     */
    private long getSongIndex(int songNumber, long locationIndexTable)
        throws java.io.IOException,
               java.io.FileNotFoundException {
        // index of song into table of song ofsets
        int indexTableIdx = songNumber & 0xFF;
        debug("Index into index table %X\n", indexTableIdx);

        // translate virtual address to physical address
        int whichFile = (int) (locationIndexTable / FILE_SIZE);
        long indexTableStart =  locationIndexTable % FILE_SIZE;
        debug("Which file %d index into file %X\n", whichFile, indexTableStart);

        File songDataFile = new File(DATADIR + dataFiles[whichFile]);
        FileInputStream dataStream = new FileInputStream(songDataFile);
        dataStream.skip(indexTableStart + indexTableIdx * SIZE_UINT);
        debug("Song data index is at %X\n", indexTableStart + indexTableIdx*SIZE_UINT);

        long songStart = readUInt(dataStream) + indexTableStart;

        return songStart + whichFile * FILE_SIZE;
    }

    private RawSong readRawSongData(long locationSongDataBlock)
        throws java.io.IOException {
        int whichFile = (int) (locationSongDataBlock / FILE_SIZE);
        long dataStart =  locationSongDataBlock % FILE_SIZE;
        debug("Which song file %d  into file %X\n", whichFile, dataStart);

        File songDataFile = new File(DATADIR + dataFiles[whichFile]);
        FileInputStream dataStream = new FileInputStream(songDataFile);
        dataStream.skip(dataStart);

        RawSong rs = new RawSong();
        rs.type = readUShort(dataStream);
        rs.compressedLyricLength = readUShort(dataStream);
        // discard next short
        readUShort(dataStream);
        rs.uncompressedLyricLength = readUShort(dataStream);
        debug("Type %X, cLength %X uLength %X\n", rs.type, rs.compressedLyricLength, rs.uncompressedLyricLength);

        // don't know what the next word is for, skip it
        //dataStream.skip(4);
        readUInt(dataStream);

        // get the compressed lyric
        rs.lyric = new byte[rs.compressedLyricLength];
        dataStream.read(rs.lyric);

        long toBoundary = 0;
        long songLength = 0;
        long uncompressedSongLength = 0;

        // get the song data

        if (rs.type == 0) {
            // Midi file starts in 4 bytes time
            songLength = readUInt(dataStream);
            uncompressedSongLength = readUInt(dataStream);
            System.out.printf("Song data length %d, uncompressed %d\n",
                              songLength, uncompressedSongLength);
            rs.uncompressedSongLength = uncompressedSongLength;

            // next word is language again?
            //toBoundary = 4;
            //dataStream.skip(toBoundary);
            readUInt(dataStream);
        } else {
            // WMA starts on next 16-byte boundary
            if( (dataStart + rs.compressedLyricLength + 12) % 16 != 0) {
                // dataStart already on 16-byte boundary, so just need extra since then
                toBoundary = 16 - ((rs.compressedLyricLength + 12) % 16);
                debug("Read lyric data to %X\n", dataStart + rs.compressedLyricLength + 12);
                debug("Length %X to boundary %X\n", rs.compressedLyricLength, toBoundary);
                dataStream.skip(toBoundary);
            }
            songLength = readUInt(dataStream);
        }

        rs.music = new byte[(int) songLength];
        dataStream.read(rs.music);

        return rs;
    }

    private long readUInt(InputStream is) throws IOException {
        long val = 0;
        for (int n = 0; n < SIZE_UINT; n++) {
            int c = is.read();
            val = (val << 8) + c;
        }
        debug("ReadUInt %X\n", val);

        return val;
    }

    private int readUShort(InputStream is) throws IOException {
        int val = 0;
        for (int n = 0; n < SIZE_USHORT; n++) {
            int c = is.read();
            val = (val << 8) + c;
        }
        debug("ReadUShort %X\n", val);
        return val;
    }

    void debug(String f, Object ...args) {
        if (DEBUG) {
            System.out.printf("Debug: " + f, args);
        }
    }

    public Song getSong(RawSong rs) {
        Song song;
        if (rs.type == 0x8000) {
            song = new WMASong(rs);
        } else {
            song = new MidiSong(rs);
        }
        return song;
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java SongExtractor <song numnber>");
            System.exit(1);
        }

        SongExtracter se = new SongExtracter();
        try {
            RawSong rs = se.getRawSong(Integer.parseInt(args[0]));

            rs.dumpToFile(args[0]);

            Song song = se.getSong(rs);
            song.dumpToFile(args[0]);
            song.dumpLyric();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    private class RawSong {
        /**
         * type == 0x0 is Midi
         * type == 0x8000 is WMA
         */
        public int type;
        public int compressedLyricLength;
        public int uncompressedLyricLength;
        public long uncompressedSongLength; // only needed for compressed Midi
        public byte[] lyric;
        public byte[] music;

        public void dumpToFile(String fileName) throws IOException {
            FileOutputStream fout = new FileOutputStream(SONGDIR + fileName + ".lyric");
            fout.write(lyric);
            fout.close();

            fout = new FileOutputStream(SONGDIR + fileName + ".music");
            fout.write(music);
            fout.close();
        }
    }

    private class Song {
        public int type;
        public byte[] lyric;
        public byte[] music;
        protected Sequence sequence;
        protected int language = -1;

        public Song(RawSong rs) {

            type = rs.type;
            lyric = decodeLyric(rs.lyric,
                                rs.uncompressedLyricLength);
        }

        /**
         * Raw lyric is LZW compressed. Decompress it
         */
        public byte[] decodeLyric(byte[] compressedLyric, long uncompressedLength) {
            // uclen is short by at least 2 - other code adds 10 so we do too
            // TODO: change LZW to use a Vector to build result so we don't have to guess at length
            byte[] result = new byte[(int) uncompressedLength + 10];
            LZW lzw = new LZW();
            int len = lzw.expand(compressedLyric, compressedLyric.length, result);
            System.out.printf("uncompressedLength %d, actual %d\n", uncompressedLength, len);
            lyric = new byte[len];
            System.arraycopy(result, 0, lyric, 0, (int) uncompressedLength);
            return lyric;
        }

        public void dumpToFile(String fileName) throws IOException {
            FileOutputStream fout = new FileOutputStream(SONGDIR + fileName + ".decodedlyric");
            fout.write(lyric);
            fout.close();

            fout = new FileOutputStream(SONGDIR + fileName + ".decodedmusic");
            fout.write(music);

            fout.close();

            fout = new FileOutputStream(SONGDIR + fileName + ".mid");
            if (sequence == null)  {
                System.out.println("Seq is null");
            } else {
                // type is MIDI type 0
                MidiSystem.write(sequence, 0, fout);
            }
        }

        public void dumpLyric() {
            for (int n = 0; n < lyric.length; n += 4) {
                if (lyric[n] == '\r') {
                    System.out.println();
                } else {
                    System.out.printf("%c", lyric[n] & 0xFF);
                }
            }
            System.out.println();
            System.out.printf("Language is %X\n", getLanguageCode());
        }

        /**
         * Lyric contains the language code as string @00@NN in header section
         */
        public int getLanguageCode() {
            int lang = 0;

            // Look for @00@NN and return NN
            for (int n = 0; n < lyric.length-20; n += 4) {
                if (lyric[n] == (byte) '@' &&
                    lyric[n+4] == (byte) '0' &&
                    lyric[n+8] == (byte) '0' &&
                    lyric[n+12] == (byte) '@') {
                    lang = ((lyric[n+16]-'0') << 4) + lyric[n+20]-'0';
                    break;

                }
            }
            return lang;
        }

        /**
         * Lyric is in a language specific encoding. Translate to Unicode UTF-8.
         * Not all languages are handled because I don't have a full set of examples
         */
        public byte[] lyricToUnicode(byte[] bytes) {
            if (language == -1) {
                language = getLanguageCode();
            }
            switch (language) {
            case SongInformation.ENGLISH:
                return bytes;

            case SongInformation.KOREAN: {
                Charset charset = Charset.forName("gb2312");
                String str = new String(bytes, charset);
                bytes = str.getBytes();
                System.out.println(str);
                return bytes;
            }

            case SongInformation.CHINESE1:
            case SongInformation.CHINESE2:
            case SongInformation.CHINESE8:
            case SongInformation.CHINESE131:
            case SongInformation.TAIWANESE3:
            case SongInformation.TAIWANESE7:
            case SongInformation.CANTONESE:
                Charset charset = Charset.forName("gb2312");
                String str = new String(bytes, charset);
                bytes = str.getBytes();
                System.out.println(str);
                return bytes;
            }
            // language not handled
            return bytes;

        }

        public void durationToOnOff() {

        }

        public Track createSequence() {
            Track track;

            try {
                sequence = new Sequence(Sequence.PPQ, 30);
            } catch(InvalidMidiDataException e) {
                // help!!!
            }
            track = sequence.createTrack();
            addLyricToTrack(track);
            return track;
        }

        public void addMsgToTrack(MidiMessage msg, Track track, long tick) {
            MidiEvent midiEvent = new MidiEvent(msg, tick);

            // No need to sort or delay insertion. From the Java API
            // "The list of events is kept in time order, meaning that this
            // event inserted at the appropriate place in the list"
            track.add(midiEvent);
        }

        /**
         * return byte as int, converting to unsigned if needed
         */
        protected int ub2i(byte b) {
            return  b >= 0 ? b : 256 + b;
        }

        public void addLyricToTrack(Track track) {
            long lastDelay = 0;
            int offset = 0;

            int data0;
            int data1;
            final int LYRIC = 0x05;
            MetaMessage msg;

            while (offset < lyric.length-4) {
                int data3 = ub2i(lyric[offset+3]);
                int data2 = ub2i(lyric[offset+2]);
                data0 = ub2i(lyric[offset]);
                data1 = ub2i(lyric[offset+1]);

                long delay = (data3 << 8) + data2;

                offset += 4;
                byte[] data;
                int len;
                long tick;

                //System.out.printf("Lyric offset %X char %X after %d with delay %d made of %d %d\n", offset, data0, lastDelay, delay, lyric[offset-1], lyric[offset-2]);

                if (data1 == 0) {
                    data = new byte[] {(byte) data0}; //, (byte) MetaMessage.META};
                } else {
                    data = new byte[] {(byte) data0, (byte) data1}; // , (byte) MetaMessage.META};
                }
                data = lyricToUnicode(data);

                msg = new MetaMessage();

                if (delay > 0) {
                    tick = delay;
                    lastDelay = delay;
                } else {
                    tick = lastDelay;
                }

                try {
                    msg.setMessage(LYRIC, data, data.length);
                } catch(InvalidMidiDataException e) {
                    e.printStackTrace();
                    continue;

                }
                addMsgToTrack(msg, track, tick);
            }
        }

    }

    private class WMASong extends Song {

        public WMASong(RawSong rs) {
            // We want to decode the lyric, but just copy the music data
            super(rs);
            music = rs.music;
            createSequence();
        }

        public void dumpToFile(String fileName) throws IOException {
            System.out.println("Dumping WMA to " + fileName + ".wma");
            super.dumpToFile(fileName);
            FileOutputStream fout = new FileOutputStream(fileName + ".wma");
            fout.write(music);
            fout.close();
        }

    }

    private class MidiSong extends Song {

        private String[] keyNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};

        public MidiSong(RawSong rs) {
            // We want the decoded lyric plus also need to decode the music
            // and then turn it into a Midi sequence
            super(rs);
            decodeMusic(rs);
            createSequence();

        }

        public void dumpToFile(String fileName) throws IOException {
            System.out.println("Dumping Midi to " + fileName);
            super.dumpToFile(fileName);
        }

        public String getKeyName(int nKeyNumber)
        {
            if (nKeyNumber > 127)
                {
                    return "illegal value";
                }
            else
                {
                    int     nNote = nKeyNumber % 12;
                    int     nOctave = nKeyNumber / 12;
                    return keyNames[nNote] + (nOctave - 1);
                }
        }

        public byte[] decodeMusic(RawSong rs) {
            byte[]  compressedMusic = rs.music;
            long uncompressedSongLength = rs.uncompressedSongLength;

            // TODO: change LZW to use a Vector to build result so we don't have to guess at length
            byte[] expanded = new byte[(int) uncompressedSongLength + 20];
            LZW lzw = new LZW();
            int len = lzw.expand(compressedMusic, compressedMusic.length, expanded);
            System.out.printf("Uncompressed %d, Actual %d\n", compressedMusic.length, len);
            music = new byte[len];
            System.arraycopy(expanded, 0, music, 0, (int) len);

            return music;

        }

        public Track createSequence() {
            Track track = super.createSequence();
            addMusicToTrack(track);
            return track;
        }

        public void addMusicToTrack(Track track) {
            int timeLine = 0;
            int offset = 0;
            int midiChannelNumber = 1;

            /* From http://board.midibuddy.net/showpost.php?p=533722&postcount=31
               Block of 5 bytes :
               xx xx xx xx xx
               1st byte = Delay Time
               2nd byte = Delay Time when the velocity will be 0,
               this one will generate another midi event
               with velocity 0 (see above).
               3nd byte = Event, for example 9x : Note On for channel x+1,
               cx for PrCh, bx for Par, ex for Pitch Bend....
               4th byte = Note
               5th byte = Velocity
            */
            System.out.println("Adding music to track");
            while (offset < music.length - 5) {

                int startDelayTime = ub2i(music[offset++]);
                int endDelayTime = ub2i(music[offset++]);
                int event = ub2i(music[offset++]);
                int data1 = ub2i(music[offset++]);
                int data2 = ub2i(music[offset++]);

                int tick = timeLine + startDelayTime;
                System.out.printf("Offset %X event %X timeline %d\n", offset, event & 0xFF, tick);

                ShortMessage msg = new ShortMessage();
                ShortMessage msg2 = null;

                try {
                    // For Midi event types see http://www.midi.org/techspecs/midimessages.php
                    switch (event & 0xF0) {
                    case ShortMessage.CONTROL_CHANGE:  // Control Change 0xB0
                    case ShortMessage.PITCH_BEND:  // Pitch Wheel Change 0xE0
                        msg.setMessage(event, data1, data2);
                        /*
                          writeChannel(midiChannelNumber, chunk[2], false);
                          writeChannel(midiChannelNumber, chunk[3], false);
                          writeChannel(midiChannelNumber, chunk[4], false);
                        */
                        break;

                    case ShortMessage.PROGRAM_CHANGE: // Program Change 0xC0
                    case ShortMessage.CHANNEL_PRESSURE: // Channel Pressure (After-touch) 0xD0
                        msg.setMessage(event, data1, 0);
                        break;

                    case 0x00:
                        // case 0x90:
                        // Note on
                        int note = data1;
                        int velocity = data2;

                        /* We have to generate a pair of note on/note off.
                           The C code manages getting the order of events

                           done correctly by keeping a list of note off events
                           and sticking them into the Midi sequence when appropriate.
                           The Java add() looks after timing for us, so we'll
                           generate a note off first and add it, and then do the note on
                        */
                        System.out.printf("Note on %s at %d, off at %d at offset %X channel %d\n",
                                          getKeyName(note),
                                          tick, tick + endDelayTime, offset, (event &0xF)+1);
                        // ON
                        msg.setMessage(ShortMessage.NOTE_ON | (event & 0xF),
                                       note, velocity);

                        // OFF
                        msg2 = new ShortMessage();
                        msg2.setMessage(ShortMessage.NOTE_OFF  | (event & 0xF),
                                        note, velocity);

                        break;

                    case 0xF0: // System Exclusive
                        // We'll write the data as is to the buffer
                        offset -= 3;
                        // msg = SysexMessage();
                        while (music[offset] != (byte) 0xF7) // bytes only go upto 127 GRRRR!!!
                            {
                                //writeChannel(midiChannelNumber, midiData[midiOffset], false);
                                System.out.printf("sysex: %x\n", music[offset]);
                                offset++;
                                if (offset >= music.length) {
                                    System.err.println("Run off end of array while processing Sysex");
                                    break;
                                }

                            }
                        //writeChannel(midiChannelNumber, midiData[midiOffset], false);
                        offset++;
                        System.out.printf("Ignoring sysex %02X\n", event);

                        // ignore the message for now
                        continue;
                        // break;

                    default:
                        System.out.printf("Unrecognized code %02X\n", event);
                        continue;
                    }
                } catch(InvalidMidiDataException e) {
                    e.printStackTrace();
                }

                addMsgToTrack(msg, track, tick);
                if (msg2 != null ) {
                    if (endDelayTime <= 0) System.out.println("Start and end at same time");
                    addMsgToTrack(msg2, track, tick + endDelayTime);
                    msg2 = null;
                }

                timeLine = tick;
            }
        }
    }
}

支持类在LZW.java里。

/**
 * Based on code by Mark Nelson
 * http://marknelson.us/1989/10/01/lzw-data-compression/
 */

public class LZW {

    private final int BITS = 12;                   /* Setting the number of bits to 12, 13*/
    private final int HASHING_SHIFT = (BITS-8);    /* or 14 affects several constants.    */
    private final int MAX_VALUE = (1 << BITS) - 1; /* Note that MS-DOS machines need to   */
    private final int MAX_CODE = MAX_VALUE - 1;    /* compile their code in large model if*/
    /* 14 bits are selected.               */

    private final int TABLE_SIZE = 5021;           /* The string table size needs to be a */
    /* prime number that is somewhat larger*/
    /* than 2**BITS.                       */
    private final int NEXT_CODE = 257;

    private long[] prefix_code = new long[TABLE_SIZE];;        /* This array holds the prefix codes   */
    private int[] append_character = new int[TABLE_SIZE];      /* This array holds the appended chars */
    private int[] decode_stack; /* This array holds the decoded string */

    private int input_bit_count=0;
    private long input_bit_buffer=0; // must be 32 bits
    private int offset = 0;

    /*
    ** This routine simply decodes a string from the string table, storing
    ** it in a buffer.  The buffer can then be output in reverse order by
    ** the expansion program.
    */
    /* JN: returns size of buffer used
     */
    private int decode_string(int idx, long code)
    {
        int i;

        i=0;
        while (code > (NEXT_CODE - 1))
            {
                decode_stack[idx++] = append_character[(int) code];
                code=prefix_code[(int) code];
                if (i++>=MAX_CODE)
                    {
                        System.err.printf("Fatal error during code expansion.\n");
                        return 0;
                    }
            }

        decode_stack[idx]= (int) code;

        return idx;
    }

    /*
    ** The following two routines are used to output variable length
    ** codes.  They are written strictly for clarity, and are not
    ** particularyl efficient.
    */

    long input_code(byte[] inputBuffer, int inputLength, int dummy_offset, boolean firstTime)
    {
        long return_value;

        //int pOffsetIdx = 0;
        if (firstTime)

            {
                input_bit_count = 0;
                input_bit_buffer = 0;
            }

        while (input_bit_count <= 24 && offset < inputLength)
            {
                /*
                input_bit_buffer |= (long) inputBuffer[offset++] << (24 - input_bit_count);
                input_bit_buffer &= 0xFFFFFFFFL;
                System.out.printf("input buffer %d\n", (long) inputBuffer[offset]);
                */
                // Java doesn't have unsigned types. Have to play stupid games when mixing
                // shifts and type coercions
                long val = inputBuffer[offset++];
                if (val < 0) {
                    val = 256 + val;
                }
                // System.out.printf("input buffer: %d\n", val);
                //if ( ((long) inpu) < 0) System.out.println("Byte is -ve???");
                input_bit_buffer |= (((long) val) << (24 - input_bit_count)) & 0xFFFFFFFFL;
                //input_bit_buffer &= 0xFFFFFFFFL;
                // System.out.printf("input bit buffer %d\n", input_bit_buffer);

                /*
                if (input_bit_buffer < 0) {
                    System.err.println("Negative!!!");
                }
                */

                input_bit_count  += 8;
            }

        if (offset >= inputLength && input_bit_count < 12)
            return MAX_VALUE;

        return_value       = input_bit_buffer >>> (32 - BITS);
        input_bit_buffer <<= BITS;
        input_bit_buffer &= 0xFFFFFFFFL;
        input_bit_count   -= BITS;

        return return_value;
    }

    void dumpLyric(int data)
    {
        System.out.printf("LZW: %d\n", data);
        if (data == 0xd)
            System.out.printf("\n");
    }

    /*
    **  This is the expansion routine.  It takes an LZW format file, and expands
    **  it to an output file.  The code here should be a fairly close match to
    **  the algorithm in the accompanying article.
    */

    public int expand(byte[] intputBuffer, int inputBufferSize, byte[] outBuffer)
    {
        long next_code = NEXT_CODE;/* This is the next available code to define */
        long new_code;
        long old_code;
        int character;
        int string_idx;

        int offsetOut = 0;

        prefix_code      = new long[TABLE_SIZE];
        append_character = new int[TABLE_SIZE];
        decode_stack     = new int[4000];

        old_code= input_code(intputBuffer, inputBufferSize, offset, true);  /* Read in the first code, initialize the */
        character = (int) old_code;          /* character variable, and send the first */
        outBuffer[offsetOut++] = (byte) old_code;       /* code to the output file                */
        //outTest(output, old_code);
        // dumpLyric((int) old_code);

        /*
        **  This is the main expansion loop.  It reads in characters from the LZW file
        **  until it sees the special code used to inidicate the end of the data.
        */
        while ((new_code=input_code(intputBuffer, inputBufferSize, offset, false)) != (MAX_VALUE))
            {
                // dumpLyric((int)new_code);
                /*
                ** This code checks for the special STRING+CHARACTER+STRING+CHARACTER+STRING
                ** case which generates an undefined code.  It handles it by decoding
                ** the last code, and adding a single character to the end of the decode string.
                */

                if (new_code>=next_code)
                    {
                        if (new_code > next_code)
                            {
                                System.err.printf("Invalid code: offset:%X new:%X next:%X\n", offset, new_code, next_code);
                                break;
                            }

                        decode_stack[0]= (int) character;
                        string_idx=decode_string(1, old_code);
                    }
                else
                    {
                        /*
                        ** Otherwise we do a straight decode of the new code.
                        */

                        string_idx=decode_string(0,new_code);
                    }

                /*
                ** Now we output the decoded string in reverse order.
                */
                character=decode_stack[string_idx];
                while (string_idx >= 0)
                    {
                        int data = decode_stack[string_idx--];
                        outBuffer[offsetOut] = (byte) data;
                        //outTest(output, *string--);

                        if (offsetOut % 4 == 0) {
                            //dumpLyric(data);
                        }

                        offsetOut++;
                    }

                /*
                ** Finally, if possible, add a new code to the string table.
                */
                if (next_code > 0xfff)
                    {
                        next_code = NEXT_CODE;
                        System.err.printf("*");
                    }

                // test code
                if (next_code > 0xff0 || next_code < 0x10f)
                    {
                        Debug.printf("%02X ", new_code);
                    }

                prefix_code[(int) next_code]=old_code;
                append_character[(int) next_code] = (int) character;
                next_code++;

                old_code=new_code;
            }
        Debug.printf("offset out is %d\n", offsetOut);
        return offsetOut;

    }
}

这里是SongInformation.java:

public class SongInformation {

    // Public fields of each song record
    /**
     *  Song number in the file, one less than in songbook
     */
    public long number;

    /**
     * song title in Unicode
     */
    public String title;

    /**
     * artist in Unicode
     */
    public String artist;

    /**
     * integer value of language code
     */
    public int language;

    public static final int  KOREAN = 0;
    public static final int  CHINESE1 = 1;
    public static final int  CHINESE2 = 2;
    public static final int  TAIWANESE3 = 3 ;
    public static final int  JAPANESE = 4;
    public static final int  RUSSIAN = 5;
    public static final int  THAI = 6;
    public static final int  TAIWANESE7 = 7;
    public static final int  CHINESE8 = 8;
    public static final int  CANTONESE = 9;
    public static final int  ENGLISH = 0x12;
    public static final int  VIETNAMESE = 0x13;
    public static final int  PHILIPPINE = 0x14;
    public static final int  TURKEY = 0x15;
    public static final int  SPANISH = 0x16;
    public static final int  INDONESIAN = 0x17;
    public static final int  MALAYSIAN = 0x18;
    public static final int  PORTUGUESE = 0x19;
    public static final int  FRENCH = 0x20;
    public static final int  INDIAN = 0x21;
    public static final int  BRASIL = 0x22;
    public static final int  CHINESE131 = 131;
    public static final int  ENGLISH146 = 146;
    public static final int  PHILIPPINE148 = 148;

    public SongInformation(long number,
                           String title,
                           String artist,
                           int language) {
        this.number = number;
        this.title = title;
        this.artist = artist;
        this.language = language;
    }

    public String toString() {
        return "" + (number+1) + " (" + language + ") \"" + title + "\" " + artist;
    }

    public boolean titleMatch(String pattern) {
        // System.out.println("Pattern: " + pattern);
        return title.matches("(?i).*" + pattern + ".*");
    }

    public boolean artistMatch(String pattern) {
        return artist.matches("(?i).*" + pattern + ".*");
    }

    public boolean numberMatch(String pattern) {
        Long n;
        try {
            n = Long.parseLong(pattern) - 1;
            //System.out.println("Long is " + n);
        } catch(Exception e) {
            //System.out.println(e.toString());
            return false;
        }
        return number == n;
    }

    public boolean languageMatch(int lang) {
        return language == lang;
    }
}

这里是Debug.java:

public class Debug {

    public static final boolean DEBUG = false;

    public static void println(String str) {
        if (DEBUG) {
            System.out.println(str);
        }
    }

    public static void printf(String format, Object... args) {
        if (DEBUG) {
            System.out.printf(format, args);
        }
    }
}

要编译这些代码,请运行以下命令:

    javac SongExtracter.java LZW.java Debug.java SongInformation.java

使用以下命令运行:

java SongExtracter <song number >

把这些 MIDI 文件转换成卡拉 KAR 文件的程序是KARConverter.java

      /*
 * KARConverter.java
 *
 * The output from decodnig the Sonken data is not in
 * the format required by the KAR "standard".
 * e.g. we need @T for the title,
 * and LYRIC events need to be changed to TEXT events
 * Tempo has to be changed too
 *
 */

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.sound.midi.MidiSystem;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.Receiver;

public class KARConverter {
    private static int LYRIC = 5;
    private static int TEXT = 1;

    private static boolean firstLyricEvent = true;

    public static void main(String[] args) {
        if (args.length != 1) {
            out("KARConverter: usage:");
            out("\tjava KARConverter <file>");
            System.exit(1);
        }
        /*
         *      args[0] is the common prefix of the two files
         */
        File    inFile = new File(args[0] + ".mid");
        File    outFile = new File(args[0] + ".kar");

        /*
         *      We try to get a Sequence object, which the content
         *      of the MIDI file.
         */
        Sequence        inSequence = null;
        Sequence        outSequence = null;
        try {
            inSequence = MidiSystem.getSequence(inFile);
        } catch (InvalidMidiDataException e) {
            e.printStackTrace();
            System.exit(1);
        } catch (IOException e) {

            e.printStackTrace();
            System.exit(1);
        }

        if (inSequence == null) {
            out("Cannot retrieve Sequence.");
        } else {
            try {
                outSequence = new Sequence(inSequence.getDivisionType(),
                                           inSequence.getResolution());
            } catch(InvalidMidiDataException e) {
                e.printStackTrace();
                System.exit(1);
            }

            createFirstTrack(outSequence);
            Track[]     tracks = inSequence.getTracks();
            fixTrack(tracks[0], outSequence);
        }
        FileOutputStream outStream = null;
        try {
            outStream = new FileOutputStream(outFile);
            MidiSystem.write(outSequence, 1, outStream);
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public static void fixTrack(Track oldTrack, Sequence seq) {
        Track lyricTrack = seq.createTrack();
        Track dataTrack = seq.createTrack();

        int nEvent = fixHeader(oldTrack, lyricTrack);
        System.out.println("nEvent " + nEvent);
        for ( ; nEvent < oldTrack.size(); nEvent++) {
            MidiEvent event = oldTrack.get(nEvent);
            if (isLyricEvent(event)) {
                event = convertLyricToText(event);
                lyricTrack.add(event);
            } else {
                dataTrack.add(event);
            }
        }

    }

    public static int fixHeader(Track oldTrack, Track lyricTrack) {
        int nEvent;

        // events at 0-10 are meaningless
        // events at 11, 12 should be the language code,
        // but maybe at 12, 13
        nEvent = 11;
        MetaMessage lang1 = (MetaMessage) (oldTrack.get(nEvent).getMessage());
        String val = new String(lang1.getData());
        if (val.equals("@")) {
            // try 12
            lang1 = (MetaMessage) (oldTrack.get(++nEvent).getMessage());
        }
        MetaMessage lang2 = (MetaMessage) (oldTrack.get(++nEvent).getMessage());
        String lang = new String(lang1.getData()) +
            new String(lang2.getData());
        System.out.println("Lang " + lang);
        byte[] karLang = getKARLang(lang);

        MetaMessage msg = new MetaMessage();
        try {
            msg.setMessage(TEXT, karLang, karLang.length);
            MidiEvent evt = new MidiEvent(msg, 0L);
            lyricTrack.add(evt);
        } catch(InvalidMidiDataException e) {
        }

        // song title is next
        StringBuffer titleBuff = new StringBuffer();
        for (nEvent = 15; nEvent < oldTrack.size(); nEvent++) {
            MidiEvent event = oldTrack.get(nEvent);
            msg = (MetaMessage) (event.getMessage());
            String contents = new String(msg.getData());
            if (contents.equals("@")) {
                break;

            }
            if (contents.equals("\r\n")) {
                continue;
            }
            titleBuff.append(contents);
        }
        String title = "@T" + titleBuff.toString();
        System.out.println("Title '" + title +"'");
        byte[] titleBytes = title.getBytes();

        msg = new MetaMessage();
        try {
            msg.setMessage(TEXT, titleBytes, titleBytes.length);
            MidiEvent evt = new MidiEvent(msg, 0L);
            lyricTrack.add(evt);
        } catch(InvalidMidiDataException e) {
        }

        // skip the next 2 @'s
        for (int skip = 0; skip < 2; skip++) {
            for (++nEvent; nEvent < oldTrack.size(); nEvent++) {
                MidiEvent event = oldTrack.get(nEvent);
                msg = (MetaMessage) (event.getMessage());
                String contents = new String(msg.getData());
                if (contents.equals("@")) {
                    break;
                }
            }
        }

        // then the singer
        StringBuffer singerBuff = new StringBuffer();
        for (++nEvent; nEvent < oldTrack.size(); nEvent++) {
            MidiEvent event = oldTrack.get(nEvent);
            if (event.getTick() != 0) {
                break;
            }
            if (! isLyricEvent(event)) {
                break;

            }

            msg = (MetaMessage) (event.getMessage());
            String contents = new String(msg.getData());
            if (contents.equals("\r\n")) {
                continue;
            }
            singerBuff.append(contents);
        }
        String singer = "@T" + singerBuff.toString();
        System.out.println("Singer '" + singer +"'");

        byte[] singerBytes = singer.getBytes();

        msg = new MetaMessage();
        try {
            msg.setMessage(1, singerBytes, singerBytes.length);
            MidiEvent evt = new MidiEvent(msg, 0L);
            lyricTrack.add(evt);
        } catch(InvalidMidiDataException e) {
        }

        return nEvent;
    }

    public static boolean isLyricEvent(MidiEvent event) {
        if (event.getMessage() instanceof MetaMessage) {
            MetaMessage msg = (MetaMessage) (event.getMessage());
            if (msg.getType() == LYRIC) {
                return true;
            }
        }
        return false;
    }

    public static MidiEvent convertLyricToText(MidiEvent event) {
        if (event.getMessage() instanceof MetaMessage) {
            MetaMessage msg = (MetaMessage) (event.getMessage());

            if (msg.getType() == LYRIC) {
                byte[] newMsgData = null;
                if (firstLyricEvent) {
                    // need to stick a \ at the front
                    newMsgData = new byte[msg.getData().length + 1];
                    System.arraycopy(msg.getData(), 0, newMsgData, 1, msg.getData().length);
                    newMsgData[0] = '\\';
                    firstLyricEvent = false;
                } else {
                    newMsgData = msg.getData();
                    if ((new String(newMsgData)).equals("\r\n")) {
                        newMsgData = "\\".getBytes();
                    }
                }
                try {
                    /*
                    msg.setMessage(TEXT,
                                   msg.getData(),
                                   msg.getData().length);
                    */
                    msg.setMessage(TEXT,
                                   newMsgData,
                                   newMsgData.length);
                } catch(InvalidMidiDataException e) {
                    e.printStackTrace();
                }
            }
        }
        return event;
    }

    public static byte[] getKARLang(String lang) {
        System.out.println("lang is " + lang);
        if (lang.equals("12")) {
            return "@LENG".getBytes();
        }

        // don't know any other language specs, so guess
        if (lang.equals("01")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("02")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("08")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("09")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("07")) {
            return "@LCHI".getBytes();
        }
        if (lang.equals("")) {
            return "@L".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }
        if (lang.equals("")) {
            return "@LENG".getBytes();
        }

        return ("@L" + lang).getBytes();
    }

    public static void copyNotesTrack(Track oldTrack, Sequence seq) {
        Track newTrack = seq.createTrack();

        for (int nEvent = 0; nEvent < oldTrack.size(); nEvent++)
            {
                MidiEvent event = oldTrack.get(nEvent);

                newTrack.add(event);
            }

    }

    public static void createFirstTrack(Sequence sequence) {
        Track track = sequence.createTrack();
        MetaMessage msg1 = new MetaMessage();
        MetaMessage msg2 = new MetaMessage();

        byte data[] = "Soft Karaoke".getBytes();
        try {
            msg1.setMessage(3, data, data.length);
        } catch(InvalidMidiDataException e) {
            e.printStackTrace();
            return;
        }
        MidiEvent event = new MidiEvent(msg1, 0L);
        track.add(event);

        data = "@KMIDI KARAOKE FILE".getBytes();
        try {
            msg2.setMessage(1, data, data.length);
        } catch(InvalidMidiDataException e) {
            e.printStackTrace();
            return;
        }
        MidiEvent event2 = new MidiEvent(msg2, 0L);
        track.add(event2);
    }

    public static void output(MidiEvent event)
    {
        MidiMessage     message = event.getMessage();
        long            lTicks = event.getTick();
    }

    private static void out(String strMessage)
    {
        System.out.println(strMessage);
    }

}

/*** KARConverter.java ***/

播放 MIDI 文件

从光盘中提取的 MIDI 文件可以使用标准的 MIDI 播放器播放,例如 Timothy。歌词包括在内,旋律线在 MIDI 通道 1。我已经用 Swing 和 Java Sound framework 编写了一批 Java 程序,它们可以播放 MIDI 文件并对其进行处理。在播放 MIDI 文件的同时,我还可以做一些很酷的 Karaoke 的事情比如显示歌词,显示应该播放的音符,通过歌词显示进度。

播放 WMA 文件

WMA 档案是“邪恶的”它们基于两种微软专有格式。第一种是高级系统格式(ASF)文件格式,它描述了音乐数据的“容器”。第二个是 Windows Media Audio 9 编解码器。

ASF 是首要问题。微软有一个公开的规范( www.microsoft.com/en-us/download/details.aspx?id=14995 ),强烈反对任何开源的东西。许可证规定,如果您基于该规范构建一个实现,那么您:

  • 无法分发源代码
  • 只能分发目标代码
  • 除非作为“解决方案”的一部分,否则不能分发目标代码(换句话说,库似乎是被禁止的)
  • 不能免费分发您的目标代码
  • 无法将您的许可证设置为允许衍生作品

更何况 2012 年 1 月 1 日之后不允许你开始任何新的执行,而且已经是 2017 年 1 月了!

只是说的更难听一点,微软有专利 6041345,“用于容纳多个媒体流的活动流格式”( www.google.com/patents/US6041345 ),是 1997 年申请的。该专利似乎覆盖了与当时存在的许多其他格式相同的领域,因此该专利的地位(如果受到质疑)尚不清楚。但是,它已经被用来阻止 GPL 授权的项目 VirtualDub ( www.advogato.org/article/101.html )支持 ASF。无论如何,文件格式的专利状态有点可疑,但在 Oracle 赢得或失去 Java API 的专利声明后,可能会变得更加清晰。

尽管如此,FFmpeg 项目( http://ffmpeg.org/ )还是完成了 ASF 的净室实现,对文件格式进行逆向工程,并且根本不使用 ASF 规范。它还逆向工程 WMA 编解码器。这使得像 MPlayer 和 VLC 这样的播放器可以播放 ASF/WMA 文件。FFmpeg 本身也可以从 ASF/WMA 转换成更好的格式,比如 Ogg Vorbis。

没有用于 WMA 文件的 Java 处理程序,考虑到许可,除非它是基于 FFmpeg 的,否则不太可能有。

我从 DVD 中提取的 WMA 文件具有以下特征:

  • 每个文件有两个通道。
  • 每个声道传送一个单声道信号。
  • 右声道承载所有乐器、伴唱以及主唱。
  • 左声道承载所有乐器和伴唱,但不承载主唱。

如果没有人对着麦克风唱歌,Sonken player 会播放右声道,但一旦有人对着麦克风唱歌,就会切换到左声道(有效地静音主唱)。简单有效。

歌词仍然作为 MIDI 存在于音轨数据中,并且可以像以前一样被提取。它们可以由 MIDI 播放器播放。我还不知道如何同步播放 MIDI 和 WMA 文件。

KAR 格式

生成的 MIDI 文件不是 KAR 格式。这意味着 pykaraoke 等 Karaoke 播放器可能会在播放它们时出现问题。将文件转换成这种格式并不太难:在序列中循环,适当地编写或修改 MIDI 事件。这个程序不是很令人兴奋,但是可以作为 KARConverter 下载。

与 pykar 一起播放歌曲

播放卡拉 MIDI 文件最简单的方法之一是使用 pykar ( www.kibosh.org/pykaraoke/ )。遗憾的是,从 Sonken 光盘中翻录的歌曲无法正常播放。这是因为 pykar 中的错误和未提供的所需特性的混合。问题及其解决方案如下。

拍子

许多 MIDI 文件会使用元事件设定速度 0x51 来明确设定速度。这些文件通常不会。pykar 希望 MIDI 文件包含此事件,否则默认为每分钟零拍的速度。正如所料,这将丢弃 pykar 执行的所有计时计算。

正如 Sonic Spot ( www.sonicspot.com/guide/midifiles.html )解释的那样,“如果没有设定的速度事件,则假定每分钟 120 拍。”它给出了一个计算合适的速度值的公式,即 60000000/120。

这需要对一个 pykaraoke 文件进行一次更改:将pykar.py的第 190 行更改如下:

sele.Tempo = [(0, 0)]

对此:

self.Tempo = [(0, 500000)]

语言编码

文件pykdb.py声称cp1252是 Karaoke 文件的默认字符编码,并使用一种叫做DejaVuSans.t的字体,这种字体适合显示这样的字符。除了标准 ASCII 之外,这种编码还在一个字节的前 128 位中添加了各种欧洲符号,例如“”。

我不确定 pykaraoke 是从哪里得到这些信息的,但它肯定不适用于中国的 Karaoke。我不知道中文、日文、韩文等使用什么编码,但是我的代码将它们作为 Unicode UTF-8 转储。适合 Unicode 的字体是Cyberbit.ttf。(参见我在 http://jan.newmarch.name/i18n/ 的全球软件讲义中的“字体”一章)。)

文件pykdb.py需要以下几行:

        self.KarEncoding = 'cp1252'  # Default text encoding in karaoke files
        self.KarFont = FontData("DejaVuSans.ttf")

更改为以下内容:

        self.KarEncoding = 'utf-8'  # Default text encoding in karaoke files
        self.KarFont = FontData("Cyberbit.ttf")

并将Cyberbit.tt的副本复制到目录/usr/share/pykaraoke/fonts/中。

没有音符的歌曲

光盘上的一些歌曲没有 MIDI 音符,因为这些都在 WMA 文件中。MIDI 文件只有歌词。pykaraoke 只弹到最后一个音,也就是零音!所以,不放歌词。

结论

本章主要讨论了一个取证问题:当文件格式未知时,如何从 DVD 中获取信息。它与播放声音没有任何直接关系,尽管它确实给了我一个已经付费的文件的大来源。