Recherche avancée

Médias (3)

Mot : - Tags -/pdf

Autres articles (85)

  • Ajouter des informations spécifiques aux utilisateurs et autres modifications de comportement liées aux auteurs

    12 avril 2011, par

    La manière la plus simple d’ajouter des informations aux auteurs est d’installer le plugin Inscription3. Il permet également de modifier certains comportements liés aux utilisateurs (référez-vous à sa documentation pour plus d’informations).
    Il est également possible d’ajouter des champs aux auteurs en installant les plugins champs extras 2 et Interface pour champs extras.

  • Support audio et vidéo HTML5

    10 avril 2011

    MediaSPIP utilise les balises HTML5 video et audio pour la lecture de documents multimedia en profitant des dernières innovations du W3C supportées par les navigateurs modernes.
    Pour les navigateurs plus anciens, le lecteur flash Flowplayer est utilisé.
    Le lecteur HTML5 utilisé a été spécifiquement créé pour MediaSPIP : il est complètement modifiable graphiquement pour correspondre à un thème choisi.
    Ces technologies permettent de distribuer vidéo et son à la fois sur des ordinateurs conventionnels (...)

  • De l’upload à la vidéo finale [version standalone]

    31 janvier 2010, par

    Le chemin d’un document audio ou vidéo dans SPIPMotion est divisé en trois étapes distinctes.
    Upload et récupération d’informations de la vidéo source
    Dans un premier temps, il est nécessaire de créer un article SPIP et de lui joindre le document vidéo "source".
    Au moment où ce document est joint à l’article, deux actions supplémentaires au comportement normal sont exécutées : La récupération des informations techniques des flux audio et video du fichier ; La génération d’une vignette : extraction d’une (...)

Sur d’autres sites (11633)

  • Sending raw h264 video and aac audio frames to an RTMP server using ffmpeg

    15 août 2016, par codeimpaler

    I am receiving raw h264 and aac audio frames from an even driven source. I am trying to send these frames to an rtmp server.
    I started working from the ffmpeg example muxing.c which successfully sends a custom stream to the rtmp server. I figure I just need to replace their frame data with my own.I found this suggestion online. I have tried How to pack raw h264 stream to flv container and send over rtmp using ffmpeg (not command)
    and
    How to publish selfmade stream with ffmpeg and c++ to rtmp server ?
    and a few other suggestions but none have worked for me.
    I have tried to directly memcpy my byte buffer but my code keeps failing
    at ret = avcodec_encode_video2(c, &pkt, frame, &got_packet).
    Specifically, I get an invalid access error.
    For a little more context, anytime I receive a frame (which is event driven), void RTMPWriter::WriteVideoFrame(...) is called. Assume the constructor has already been called before the first frame is received.
    I am not that familiar with ffmpeg and there could be several things wrong with the code. Any input will be really appreciated.

       #define STREAM_FRAME_RATE 25 /* 25 images/s */
       #define STREAM_PIX_FMT    AV_PIX_FMT_YUV420P /* default pix_fmt */
       #define SCALE_FLAGS SWS_BICUBIC
       RTMPWriter::RTMPWriter()
         : seenKeyFrame(false),
           video_st({ 0 }),
           audio_st({ 0 }),
           have_video(0),
           have_audio(0)
       {

           const char *filename;
           AVCodec *audio_codec = NULL, *video_codec = NULL;
           int ret;

           int encode_video = 0, encode_audio = 0;
           AVDictionary *opt = NULL;
           int i;

           /* Initialize libavcodec, and register all codecs and formats. */
           av_register_all();

           avformat_network_init();

          String^ StreamURL = "StreamURL";
          String^ out_uri = safe_cast(ApplicationData::Current->LocalSettings->Values->Lookup(StreamURL));
          std::wstring out_uriW(out_uri->Begin());
          std::string out_uriA(out_uriW.begin(), out_uriW.end());
          filename = out_uriA.c_str();  

          /* allocate the output media context */
          avformat_alloc_output_context2(&oc, NULL, "flv", filename);
          if (!oc)
          {
              OutputDebugString(L"Could not deduce output format from file extension: using MPEG.\n");
              avformat_alloc_output_context2(&oc, NULL, "mpeg", filename);
          }
          if (!oc)
          {
              OutputDebugString(L"Could not allocate  using MPEG.\n");
          }


          fmt = oc->oformat;

          /* Add the audio and video streams using the default format codecs
          * and initialize the codecs. */
          if (fmt->video_codec != AV_CODEC_ID_NONE) {
              add_stream(&video_st, oc, &video_codec, fmt->video_codec);
              have_video = 1;
              encode_video = 1;
          }
          if (fmt->audio_codec != AV_CODEC_ID_NONE) {
              add_stream(&audio_st, oc, &audio_codec, fmt->audio_codec);
              have_audio = 1;
              encode_audio = 1;
          }

          /* Now that all the parameters are set, we can open the audio and
           * video codecs and allocate the necessary encode buffers. */
          if (have_video)
          {
              open_video(oc, video_codec, &video_st, opt);
          }

          if (have_audio)
          {
              open_audio(oc, audio_codec, &audio_st, opt);
          }

          av_dump_format(oc, 0, filename, 1);

          /* open the output file, if needed */
          if (!(fmt->flags & AVFMT_NOFILE))
          {
              ret = avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);
              if (ret < 0)
              {
                  OutputDebugString(L"Could not open ");
                  OutputDebugString(out_uri->Data());
              }
          }

          /* Write the stream header, if any. */
          ret = avformat_write_header(oc, &opt);
          if (ret < 0)
          {
              OutputDebugString(L"Error occurred when writing stream header \n");
          }

       }

       void RTMPWriter::WriteVideoFrame(
           boolean isKeyFrame,
           boolean hasDiscontinuity,
           UINT64 frameId,
           UINT32 videoBufferLength,
           BYTE *videoBytes)
       {

           int ret;
           AVCodecContext *c;
           AVFrame* frame;
           int got_packet = 0;
           AVPacket pkt = { 0 };

           c = video_st.enc;

           frame = get_video_frame(videoBufferLength, videoBytes);

           /* encode the image */
           ret = avcodec_encode_video2(c, &pkt, frame, &got_packet);
           if (ret < 0) {
                OutputDebugString(L"Error encoding video frame: \n")
           }

           if (got_packet)
           {
               ret = write_frame(oc, &c->time_base, video_st.st, &pkt);
           }
           else {
               ret = 0;
           }

           if (ret < 0) {
                OutputDebugString(L"Error while writing video frame: %s\n");
           }
       }

       AVFrame * RTMPWriter::get_video_frame(
          UINT32 videoBufferLength,
          BYTE *videoBytes)
       {
           AVCodecContext *c = video_st.enc;

           if (c->pix_fmt != AV_PIX_FMT_YUV420P) {
               /* as we only generate a YUV420P picture, we must convert it
               * to the codec pixel format if needed */
               if (!video_st.sws_ctx) {
                   video_st.sws_ctx = sws_getContext(c->width, c->height,
                       AV_PIX_FMT_YUV420P,
                       c->width, c->height,
                       c->pix_fmt,
                       SCALE_FLAGS, NULL, NULL, NULL);
                   if (!video_st.sws_ctx) {
                       fprintf(stderr,
                           "Could not initialize the conversion context\n");
                           exit(1);
                   }
               }
               fill_yuv_image(video_st.tmp_frame, video_st.next_pts, c->width, c->height, videoBufferLength, videoBytes);
               sws_scale(video_st.sws_ctx,
               (const uint8_t * const *)video_st.tmp_frame->data, video_st.tmp_frame->linesize,
               0, c->height, video_st.frame->data, video_st.frame->linesize);
           }
           else {
               fill_yuv_image(video_st.frame, video_st.next_pts, c->width, c->height, videoBufferLength, videoBytes);
           }

           video_st.frame->pts = video_st.next_pts++;

           return video_st.frame;
       }

       /* Prepare a dummy image. */
       void  RTMPWriter::fill_yuv_image(
            AVFrame *pict,
            int frame_index,
            int width,
            int height,
            UINT32 videoBufferLength,
            BYTE *videoBytes)
       {
           //int x, y, i, ret;

           /* when we pass a frame to the encoder, it may keep a reference to it
           * internally;
           * make sure we do not overwrite it here
           */
           ret = av_frame_make_writable(pict);
           if (ret < 0)
           {
                OutputDebugString(L"Unable to make piture writable");
           }

           memcpy(pict->data, videoBytes, videoBufferLength);

           //i = frame_index;

           ///* Y */
           //for (y = 0; y < height; y++)
           //  for (x = 0; x < width; x++)
           //      pict->data[0][y * pict->linesize[0] + x] = x + y + i * 3;

           ///* Cb and Cr */
           //for (y = 0; y < height / 2; y++) {
           //  for (x = 0; x < width / 2; x++) {
           //      pict->data[1][y * pict->linesize[1] + x] = 128 + y + i * 2;
           //      pict->data[2][y * pict->linesize[2] + x] = 64 + x + i * 5;
           //  }
           //}
       }

       void RTMPWriter::WriteAudioFrame()
       {

       }

       /* Add an output stream. */
       void  RTMPWriter::add_stream(
           OutputStream *ost,
           AVFormatContext *oc,
           AVCodec **codec,
           enum AVCodecID codec_id)
      {
       AVCodecContext *c;
       int i;

       /* find the encoder */
       *codec = avcodec_find_encoder(codec_id);
       if (!(*codec)) {
           OutputDebugString(L"Could not find encoder for '%s'\n");
           //avcodec_get_name(codec_id));
           exit(1);
       }

       ost->st = avformat_new_stream(oc, NULL);
       if (!ost->st) {
           OutputDebugString(L"Could not allocate stream\n");
           exit(1);
       }
       ost->st->id = oc->nb_streams - 1;
       c = avcodec_alloc_context3(*codec);
       if (!c) {
           OutputDebugString(L"Could not alloc an encoding context\n");
           exit(1);
       }
       ost->enc = c;

       switch ((*codec)->type) {
       case AVMEDIA_TYPE_AUDIO:
           c->sample_fmt = (*codec)->sample_fmts ?
               (*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
           c->bit_rate = 64000;
           c->sample_rate = 44100;
           if ((*codec)->supported_samplerates) {
               c->sample_rate = (*codec)->supported_samplerates[0];
               for (i = 0; (*codec)->supported_samplerates[i]; i++) {
                   if ((*codec)->supported_samplerates[i] == 44100)
                       c->sample_rate = 44100;
               }
           }
           c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
           c->channel_layout = AV_CH_LAYOUT_STEREO;
           if ((*codec)->channel_layouts) {
               c->channel_layout = (*codec)->channel_layouts[0];
               for (i = 0; (*codec)->channel_layouts[i]; i++) {
                   if ((*codec)->channel_layouts[i] == AV_CH_LAYOUT_STEREO)
                       c->channel_layout = AV_CH_LAYOUT_STEREO;
               }
           }
           c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
           ost->st->time_base = /*(AVRational)*/{ 1, c->sample_rate };
           break;

       case AVMEDIA_TYPE_VIDEO:
           c->codec_id = codec_id;

           c->bit_rate = 400000;
           /* Resolution must be a multiple of two. */
           c->width = 352;
           c->height = 288;
           /* timebase: This is the fundamental unit of time (in seconds) in terms
           * of which frame timestamps are represented. For fixed-fps content,
           * timebase should be 1/framerate and timestamp increments should be
           * identical to 1. */
           ost->st->time_base = /*(AVRational)*/{ 1, STREAM_FRAME_RATE };
           c->time_base = ost->st->time_base;

           c->gop_size = 12; /* emit one intra frame every twelve frames at most */
           c->pix_fmt = STREAM_PIX_FMT;
               if (c->codec_id == AV_CODEC_ID_MPEG2VIDEO) {
                   /* just for testing, we also add B-frames */
                   c->max_b_frames = 2;
               }
               if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO) {
                   /* Needed to avoid using macroblocks in which some coeffs overflow.
                   * This does not happen with normal video, it just happens here as
                   * the motion of the chroma plane does not match the luma plane. */
                   c->mb_decision = 2;
               }
               break;

           default:
               break;
           }

            /* Some formats want stream headers to be separate. */
           if (oc->oformat->flags & AVFMT_GLOBALHEADER)
               c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
       }

    AVFrame * RTMPWriter::alloc_audio_frame(
       enum AVSampleFormat sample_fmt,
       uint64_t channel_layout,
       int sample_rate, int nb_samples)
    {
       AVFrame *frame = av_frame_alloc();
       int ret;

       if (!frame) {
           OutputDebugString(L"Error allocating an audio frame\n");
           exit(1);
       }

       frame->format = sample_fmt;
       frame->channel_layout = channel_layout;
       frame->sample_rate = sample_rate;
       frame->nb_samples = nb_samples;

       if (nb_samples) {
           ret = av_frame_get_buffer(frame, 0);
           if (ret < 0) {
               OutputDebugString(L"Error allocating an audio buffer\n");
               exit(1);
           }
       }

           return frame;
       }




    void  RTMPWriter::open_audio(
       AVFormatContext *oc,
       AVCodec *codec,
       OutputStream *ost,
       AVDictionary *opt_arg)
    {
       AVCodecContext *c;
       int nb_samples;
       int ret;
       AVDictionary *opt = NULL;

       c = ost->enc;

       /* open it */
       av_dict_copy(&opt, opt_arg, 0);
       ret = avcodec_open2(c, codec, &opt);
       av_dict_free(&opt);
       if (ret < 0) {
           OutputDebugString(L"Could not open audio codec: %s\n");// , av_err2str(ret));
           exit(1);
       }

       /* init signal generator */
       ost->t = 0;
       ost->tincr = 2 * M_PI * 110.0 / c->sample_rate;
       /* increment frequency by 110 Hz per second */
       ost->tincr2 = 2 * M_PI * 110.0 / c->sample_rate / c->sample_rate;

       if (c->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)
           nb_samples = 10000;
       else
           nb_samples = c->frame_size;

       ost->frame = alloc_audio_frame(c->sample_fmt, c->channel_layout,
           c->sample_rate, nb_samples);
       ost->tmp_frame = alloc_audio_frame(AV_SAMPLE_FMT_S16, c->channel_layout,
           c->sample_rate, nb_samples);

       /* copy the stream parameters to the muxer */
       ret = avcodec_parameters_from_context(ost->st->codecpar, c);
       if (ret < 0) {
           OutputDebugString(L"Could not copy the stream parameters\n");
           exit(1);
       }

       /* create resampler context */
       ost->swr_ctx = swr_alloc();
       if (!ost->swr_ctx) {
           OutputDebugString(L"Could not allocate resampler context\n");
           exit(1);
       }

       /* set options */
       av_opt_set_int(ost->swr_ctx, "in_channel_count", c->channels, 0);
       av_opt_set_int(ost->swr_ctx, "in_sample_rate", c->sample_rate, 0);
       av_opt_set_sample_fmt(ost->swr_ctx, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
       av_opt_set_int(ost->swr_ctx, "out_channel_count", c->channels, 0);
       av_opt_set_int(ost->swr_ctx, "out_sample_rate", c->sample_rate, 0);
       av_opt_set_sample_fmt(ost->swr_ctx, "out_sample_fmt", c->sample_fmt, 0);

       /* initialize the resampling context */
       if ((ret = swr_init(ost->swr_ctx)) < 0) {
           OutputDebugString(L"Failed to initialize the resampling context\n");
           exit(1);
       }
    }

    int RTMPWriter::write_frame(
       AVFormatContext *fmt_ctx,
       const AVRational *time_base,
       AVStream *st,
       AVPacket *pkt)
    {
       /* rescale output packet timestamp values from codec to stream timebase */
       av_packet_rescale_ts(pkt, *time_base, st->time_base);
       pkt->stream_index = st->index;

       /* Write the compressed frame to the media file. */
       //log_packet(fmt_ctx, pkt);
       OutputDebugString(L"Actually sending video frame: %s\n");
       return av_interleaved_write_frame(fmt_ctx, pkt);
    }


    AVFrame  *RTMPWriter::alloc_picture(
       enum AVPixelFormat pix_fmt,
       int width,
       int height)
    {
       AVFrame *picture;
       int ret;

       picture = av_frame_alloc();
       if (!picture)
           return NULL;

       picture->format = pix_fmt;
       picture->width = width;
       picture->height = height;

       /* allocate the buffers for the frame data */
       ret = av_frame_get_buffer(picture, 32);
       if (ret < 0) {
           fprintf(stderr, "Could not allocate frame data.\n");
           exit(1);
       }

       return picture;
    }

    void RTMPWriter::open_video(
       AVFormatContext *oc,
       AVCodec *codec,
       OutputStream *ost,
       AVDictionary *opt_arg)
    {
       int ret;
       AVCodecContext *c = ost->enc;
       AVDictionary *opt = NULL;

       av_dict_copy(&opt, opt_arg, 0);

       /* open the codec */
       ret = avcodec_open2(c, codec, &opt);
       av_dict_free(&opt);
       if (ret < 0) {
           OutputDebugString(L"Could not open video codec: %s\n");// , av_err2str(ret));
           exit(1);
       }

       /* allocate and init a re-usable frame */
       ost->frame = alloc_picture(c->pix_fmt, c->width, c->height);
       if (!ost->frame) {
           OutputDebugString(L"Could not allocate video frame\n");
           exit(1);
       }

       /* If the output format is not YUV420P, then a temporary YUV420P
       * picture is needed too. It is then converted to the required
       * output format. */
       ost->tmp_frame = NULL;
       if (c->pix_fmt != AV_PIX_FMT_YUV420P) {
           ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, c->width, c->height);
           if (!ost->tmp_frame) {
               OutputDebugString(L"Could not allocate temporary picture\n");
               exit(1);
           }
       }

       /* copy the stream parameters to the muxer */
       ret = avcodec_parameters_from_context(ost->st->codecpar, c);
       if (ret < 0) {
           OutputDebugString(L"Could not copy the stream parameters\n");
           exit(1);
       }
    }

    void RTMPWriter::close_stream(AVFormatContext *oc, OutputStream *ost)
    {
       avcodec_free_context(&ost->enc);
       av_frame_free(&ost->frame);
       av_frame_free(&ost->tmp_frame);
       sws_freeContext(ost->sws_ctx);
       swr_free(&ost->swr_ctx);
    }

    RTMPWriter::~RTMPWriter()
    {
       av_write_trailer(oc);
       /* Close each codec. */
       if (have_video)
           close_stream(oc, &video_st);
       if (have_audio)
           close_stream(oc, &audio_st);

       if (!(fmt->flags & AVFMT_NOFILE))
           /* Close the output file. */
           avio_closep(&oc->pb);

       /* free the stream */
       avformat_free_context(oc);
    }
  • Translating Return To Ringworld

    17 août 2016, par Multimedia Mike — Game Hacking

    As indicated in my previous post, the Translator has expressed interest in applying his hobby towards another DOS adventure game from the mid 1990s : Return to Ringworld (henceforth R2RW) by Tsunami Media. This represents significantly more work than the previous outing, Phantasmagoria.


    Return to Ringworld Title Screen
    Return to Ringworld Title Screen

    I have been largely successful thus far in crafting translation tools. I have pushed the fruits of these labors to a Github repository named improved-spoon (named using Github’s random name generator because I wanted something more interesting than ‘game-hacking-tools’).

    Further, I have recorded everything I have learned about the game’s resource format (named RLB) at the XentaxWiki.

    New Challenges
    The previous project mostly involved scribbling subtitle text on an endless series of video files by leveraging a separate software library which took care of rendering fonts. In contrast, R2RW has at least 30k words of English text contained in various blocks which require translation. Further, the game encodes its own fonts (9 of them) which stubbornly refuse to be useful for rendering text in nearly any other language.

    Thus, the immediate 2 challenges are :

    1. Translating volumes of text to Spanish
    2. Expanding the fonts to represent Spanish characters

    Normally, “figuring out the file format data structures involved” is on the list as well. Thankfully, understanding the formats is not a huge challenge since the folks at the ScummVM project already did all the heavy lifting of reverse engineering the file formats.

    The Pitch
    Here was the plan :

    • Create a tool that can dump out the interesting data from the game’s master resource file.
    • Create a tool that can perform the elaborate file copy described in the previous post. The new file should be bit for bit compatible with the original file.
    • Modify the rewriting tool to repack some modified strings into the new resource file.
    • Unpack the fonts and figure out a way to add new characters.
    • Repack the new fonts into the resource file.
    • Repack message strings with Spanish characters.

    Showing The Work : Modifying Strings
    First, I created the tool to unpack blocks of message string resources. I elected to dump the strings to disk as JSON data since it’s easy to write and read JSON using Python, and it’s quick to check if any mistakes have crept in.

    The next step is to find a string to focus on. So I started the game and looked for the first string I could trigger :


    Return to Ringworld: Original text

    This shows up in the JSON string dump as :

      
        "Spanish" : " !0205Your quarters on the Lance of Truth are spartan, in accord with your mercenary lifestyle.",
        "English" : " !0205Your quarters on the Lance of Truth are spartan, in accord with your mercenary lifestyle."
      ,
    

    As you can see, many of the strings are encoded with an ID key as part of the string which should probably be left unmodified. I changed the Spanish string :

      
        "Spanish" : " !0205Hey, is this thing on ?",
        "English" : " !0205Your quarters on the Lance of Truth are spartan, in accord with your mercenary lifestyle."
      ,
    

    And then I wrote the repacking tool to substitute this message block for the original one. Look ! The engine liked it !


    Return to Ringworld: Modified text

    Little steps, little steps.

    Showing The Work : Modifying Fonts
    The next little step is to find a place to put the new characters. First, a problem definition : The immediate goal is to translate the game into Spanish. The current fonts encoded in the game resource only support 128 characters, corresponding to 7-bit ASCII. In order to properly express Spanish, 16 new characters are required : á, é, í, ó, ú, ü, ñ (each in upper and lower case for a total of 14 characters) as well as the inverted punctuation symbols : ¿, ¡.

    Again, ScummVM already documents (via code) the font coding format. So I quickly determined that each of the 9 fonts is comprised of 128 individual bitmaps with either 1 or 2 bits per pixel. I wrote a tool to unpack each character into an individual portable grey map (PGM) image. These can be edited with graphics editors or with text editors since they are just text files.

    Where to put the 16 new Spanish characters ? ASCII characters 1-31 are non-printable, so my first theory was that these characters would be empty and could be repurposed. However, after dumping and inspecting, I learned that they represent the same set of characters as seen in DOS Code Page 437. So that’s a no-go (so I assumed ; I didn’t check if any existing strings leveraged those characters).

    My next plan was hope that I could extend the font beyond index 127 and use positions 128-143. This worked superbly. This is the new example string :

      
        "Spanish" : " !0205¿Ves esto ? ¡La puntuacion se hace girar !",
        "English" : " !0205Your quarters on the Lance of Truth are spartan, in accord with your mercenary lifestyle."
      ,
    

    Fortunately, JSON understands UTF-8 and after mapping the 16 necessary characters down to the numeric range of 128-143, I repacked the new fonts and the new string :


    Return to Ringworld: Espanol
    Translation : “See this ? The punctuation is rotated !”

    Another victory. Notice that there are no diacritics in this string. None are required for this translation (according to Google Translate). But adding the diacritics to the 14 characters isn’t my department. My tool does help by prepopulating [aeiounAEIOUN] into the right positions to make editing easier for the Translator. But the tool does make the effort to rotate the punctuation since that is easy to automate.

    Next Steps and Residual Weirdness
    There is another method for storing ASCII text inside the R2RW resource called strip resources. These store conversation scripts. There are plenty of fields in the data structures that I don’t fully understand. So, following the lessons I learned from my previous translation outing, I was determined to modify as little as possible. This means copying over most of the original data structures intact, but changing the field representing the relative offset that points to the corresponding string. This works well since the strings are invariably stored NULL-terminated in a concatenated manner.

    I wanted to document for the record that the format that R2RW uses has some weirdness in they way it handles residual bytes in a resource. The variant of the resource format that R2RW uses requires every block to be aligned on a 16-byte boundary. If there is space between the logical end of the resource and the start of the next resource, there are random bytes in that space. This leads me to believe that these bytes were originally recorded from stale/uninitialized memory. This frustrates me because when I write the initial file copy tool which unpacks and repacks each block, I want the new file to be identical to the original. However, these apparent nonsense bytes at the end thwart that effort.

    But leaving those bytes as 0 produces an acceptable resource file.

    Text On Static Images
    There is one last resource type we are working on translating. There are various bits of text that are rendered as images. For example, from the intro :


    Return to Ringworld: Static text

    It’s possible to locate and extract the exact image that is overlaid on this scene, though without the colors :


    Original static text

    The palettes are stored in a separate resource type. So it seems the challenge is to figure out the palette in use for these frames and render a transparent image that uses the same palette, then repack the new text-image into the new resource file.

  • Translating Return To Ringworld

    17 août 2016, par Multimedia Mike — Game Hacking

    As indicated in my previous post, the Translator has expressed interest in applying his hobby towards another DOS adventure game from the mid 1990s : Return to Ringworld (henceforth R2RW) by Tsunami Media. This represents significantly more work than the previous outing, Phantasmagoria.


    Return to Ringworld Title Screen
    Return to Ringworld Title Screen

    I have been largely successful thus far in crafting translation tools. I have pushed the fruits of these labors to a Github repository named improved-spoon (named using Github’s random name generator because I wanted something more interesting than ‘game-hacking-tools’).

    Further, I have recorded everything I have learned about the game’s resource format (named RLB) at the XentaxWiki.

    New Challenges
    The previous project mostly involved scribbling subtitle text on an endless series of video files by leveraging a separate software library which took care of rendering fonts. In contrast, R2RW has at least 30k words of English text contained in various blocks which require translation. Further, the game encodes its own fonts (9 of them) which stubbornly refuse to be useful for rendering text in nearly any other language.

    Thus, the immediate 2 challenges are :

    1. Translating volumes of text to Spanish
    2. Expanding the fonts to represent Spanish characters

    Normally, “figuring out the file format data structures involved” is on the list as well. Thankfully, understanding the formats is not a huge challenge since the folks at the ScummVM project already did all the heavy lifting of reverse engineering the file formats.

    The Pitch
    Here was the plan :

    • Create a tool that can dump out the interesting data from the game’s master resource file.
    • Create a tool that can perform the elaborate file copy described in the previous post. The new file should be bit for bit compatible with the original file.
    • Modify the rewriting tool to repack some modified strings into the new resource file.
    • Unpack the fonts and figure out a way to add new characters.
    • Repack the new fonts into the resource file.
    • Repack message strings with Spanish characters.

    Showing The Work : Modifying Strings
    First, I created the tool to unpack blocks of message string resources. I elected to dump the strings to disk as JSON data since it’s easy to write and read JSON using Python, and it’s quick to check if any mistakes have crept in.

    The next step is to find a string to focus on. So I started the game and looked for the first string I could trigger :


    Return to Ringworld: Original text

    This shows up in the JSON string dump as :

      
        "Spanish" : " !0205Your quarters on the Lance of Truth are spartan, in accord with your mercenary lifestyle.",
        "English" : " !0205Your quarters on the Lance of Truth are spartan, in accord with your mercenary lifestyle."
      ,
    

    As you can see, many of the strings are encoded with an ID key as part of the string which should probably be left unmodified. I changed the Spanish string :

      
        "Spanish" : " !0205Hey, is this thing on ?",
        "English" : " !0205Your quarters on the Lance of Truth are spartan, in accord with your mercenary lifestyle."
      ,
    

    And then I wrote the repacking tool to substitute this message block for the original one. Look ! The engine liked it !


    Return to Ringworld: Modified text

    Little steps, little steps.

    Showing The Work : Modifying Fonts
    The next little step is to find a place to put the new characters. First, a problem definition : The immediate goal is to translate the game into Spanish. The current fonts encoded in the game resource only support 128 characters, corresponding to 7-bit ASCII. In order to properly express Spanish, 16 new characters are required : á, é, í, ó, ú, ü, ñ (each in upper and lower case for a total of 14 characters) as well as the inverted punctuation symbols : ¿, ¡.

    Again, ScummVM already documents (via code) the font coding format. So I quickly determined that each of the 9 fonts is comprised of 128 individual bitmaps with either 1 or 2 bits per pixel. I wrote a tool to unpack each character into an individual portable grey map (PGM) image. These can be edited with graphics editors or with text editors since they are just text files.

    Where to put the 16 new Spanish characters ? ASCII characters 1-31 are non-printable, so my first theory was that these characters would be empty and could be repurposed. However, after dumping and inspecting, I learned that they represent the same set of characters as seen in DOS Code Page 437. So that’s a no-go (so I assumed ; I didn’t check if any existing strings leveraged those characters).

    My next plan was hope that I could extend the font beyond index 127 and use positions 128-143. This worked superbly. This is the new example string :

      
        "Spanish" : " !0205¿Ves esto ? ¡La puntuacion se hace girar !",
        "English" : " !0205Your quarters on the Lance of Truth are spartan, in accord with your mercenary lifestyle."
      ,
    

    Fortunately, JSON understands UTF-8 and after mapping the 16 necessary characters down to the numeric range of 128-143, I repacked the new fonts and the new string :


    Return to Ringworld: Espanol
    Translation : “See this ? The punctuation is rotated !”

    Another victory. Notice that there are no diacritics in this string. None are required for this translation (according to Google Translate). But adding the diacritics to the 14 characters isn’t my department. My tool does help by prepopulating [aeiounAEIOUN] into the right positions to make editing easier for the Translator. But the tool does make the effort to rotate the punctuation since that is easy to automate.

    Next Steps and Residual Weirdness
    There is another method for storing ASCII text inside the R2RW resource called strip resources. These store conversation scripts. There are plenty of fields in the data structures that I don’t fully understand. So, following the lessons I learned from my previous translation outing, I was determined to modify as little as possible. This means copying over most of the original data structures intact, but changing the field representing the relative offset that points to the corresponding string. This works well since the strings are invariably stored NULL-terminated in a concatenated manner.

    I wanted to document for the record that the format that R2RW uses has some weirdness in they way it handles residual bytes in a resource. The variant of the resource format that R2RW uses requires every block to be aligned on a 16-byte boundary. If there is space between the logical end of the resource and the start of the next resource, there are random bytes in that space. This leads me to believe that these bytes were originally recorded from stale/uninitialized memory. This frustrates me because when I write the initial file copy tool which unpacks and repacks each block, I want the new file to be identical to the original. However, these apparent nonsense bytes at the end thwart that effort.

    But leaving those bytes as 0 produces an acceptable resource file.

    Text On Static Images
    There is one last resource type we are working on translating. There are various bits of text that are rendered as images. For example, from the intro :


    Return to Ringworld: Static text

    It’s possible to locate and extract the exact image that is overlaid on this scene, though without the colors :


    Original static text

    The palettes are stored in a separate resource type. So it seems the challenge is to figure out the palette in use for these frames and render a transparent image that uses the same palette, then repack the new text-image into the new resource file.

    The post Translating Return To Ringworld first appeared on Breaking Eggs And Making Omelettes.