Recherche avancée

Médias (1)

Mot : - Tags -/framasoft

Autres articles (67)

  • Le profil des utilisateurs

    12 avril 2011, par

    Chaque utilisateur dispose d’une page de profil lui permettant de modifier ses informations personnelle. Dans le menu de haut de page par défaut, un élément de menu est automatiquement créé à l’initialisation de MediaSPIP, visible uniquement si le visiteur est identifié sur le site.
    L’utilisateur a accès à la modification de profil depuis sa page auteur, un lien dans la navigation "Modifier votre profil" est (...)

  • Configurer la prise en compte des langues

    15 novembre 2010, par

    Accéder à la configuration et ajouter des langues prises en compte
    Afin de configurer la prise en compte de nouvelles langues, il est nécessaire de se rendre dans la partie "Administrer" du site.
    De là, dans le menu de navigation, vous pouvez accéder à une partie "Gestion des langues" permettant d’activer la prise en compte de nouvelles langues.
    Chaque nouvelle langue ajoutée reste désactivable tant qu’aucun objet n’est créé dans cette langue. Dans ce cas, elle devient grisée dans la configuration et (...)

  • Sélection de projets utilisant MediaSPIP

    29 avril 2011, par

    Les exemples cités ci-dessous sont des éléments représentatifs d’usages spécifiques de MediaSPIP pour certains projets.
    Vous pensez avoir un site "remarquable" réalisé avec MediaSPIP ? Faites le nous savoir ici.
    Ferme MediaSPIP @ Infini
    L’Association Infini développe des activités d’accueil, de point d’accès internet, de formation, de conduite de projets innovants dans le domaine des Technologies de l’Information et de la Communication, et l’hébergement de sites. Elle joue en la matière un rôle unique (...)

Sur d’autres sites (9891)

  • Getting Error during executin native android code

    3 avril 2013, par dilipkaklotar

    Error on my console

    bash : cannot set terminal process group (-1) : Inappropriate ioctl for device
    bash : no job control in this shell
    Your group is currently "mkpasswd". This indicates that your
    gid is not in /etc/group and your uid is not in /etc/passwd.

    The /etc/passwd (and possibly /etc/group) files should be rebuilt.
    See the man pages for mkpasswd and mkgroup then, for example, run

    mkpasswd -l [-d] > /etc/passwd
    mkgroup -l [-d] > /etc/group

    Note that the -d switch is necessary for domain users.
    - ]0 ; -
    - [32mDILIP@DILIP-PC -[33m -[0m
    $

    public class VideoBrowser extends ListActivity implements ListView.OnScrollListener {

    /*this part communicates with native code through jni (java native interface)*/
    //load the native library
    static {
       System.loadLibrary("ffmpeg");
       System.loadLibrary("ffmpeg-test-jni");
    }
    //declare the jni functions
    private static native void naInit(String _videoFileName);
    private static native int[] naGetVideoResolution();
    private static native String naGetVideoCodecName();
    private static native String naGetVideoFormatName();
    private static native void naClose();

    private void showVideoInfo(final File _file) {
       String videoFilename = _file.getAbsolutePath();
       naInit(videoFilename);
       int[] prVideoRes = naGetVideoResolution();
       String prVideoCodecName = naGetVideoCodecName();
       String prVideoFormatName = naGetVideoFormatName();
       naClose();
       String displayText = "Video: " + videoFilename + "\n";
       displayText += "Video Resolution: " + prVideoRes[0] + "x" + prVideoRes[1] + "\n";
       displayText += "Video Codec: " + prVideoCodecName + "\n";
       displayText += "Video Format: " + prVideoFormatName + "\n";
       text_titlebar_text.setText(displayText);
    }


    /*the rest of the file deals with UI and other stuff*/
    private Context mContext;
    public static VideoBrowser self;

    /**
    * activity life cycle: this part of the source code deals with activity life cycle
    */
    @Override
    public void onCreate(Bundle icicle) {
       super.onCreate(icicle);
       mContext = this.getApplicationContext();
       self = this;
       initUI();
    }

    @Override
    protected void onDestroy() {
       super.onDestroy();
       unbindDisplayEntries();
    }

    public void unbindDisplayEntries() {
       if (displayEntries!=null) {
           int l_count = displayEntries.size();
           for (int i = 0; i < l_count; ++i) {
               IconifiedTextSelected l_its = displayEntries.get(i);
               if (l_its != null) {
                   Drawable l_dr = l_its.getIcon();
                   if (l_dr != null) {
                       l_dr.setCallback(null);
                       l_dr = null;
                   }
               }
           }
       }
       if (l_displayEntries!=null) {
           int l_count = l_displayEntries.size();
           for (int i = 0; i < l_count; ++i) {
               IconifiedTextSelected l_its = l_displayEntries.get(i);
               if (l_its != null) {
                   Drawable l_dr = l_its.getIcon();
                   if (l_dr != null) {
                       l_dr.setCallback(null);
                       l_dr = null;
                   }
               }
           }
       }
    }

    /**
    * Data: this part of the code deals with data processing
    */
    public List<iconifiedtextselected> displayEntries = new ArrayList<iconifiedtextselected>();
    public static List<iconifiedtextselected> l_displayEntries = new ArrayList<iconifiedtextselected>();;

    /**load images
    * this part of code deals with loading of images
    */
    private File currentDirectory;
    public int media_browser_load_option = 2;
    private static int last_media_browser_load_option = 2;
    private static int number_of_icons = 0;
    private static final String upOneLevel = "..";

    LoadVideoTask loadTask;
    private void loadVideosFromDirectory(String _dir) {
       try {
           loadTask = new LoadVideoTask();
           loadTask.execute(_dir);
       } catch (Exception e) {
           Toast.makeText(this, "Load media fail!", Toast.LENGTH_SHORT).show();
       }
    }

    private void getVideosFromDirectoryNonRecurAddParent(File _dir) {
       //add the upper one level data
       if (_dir.getParent()!=null) {
           this.displayEntries.add(new IconifiedTextSelected(
                   upOneLevel,
                   getResources().getDrawable(R.drawable.folderback),
                   false, false, 0));
       }
    }

    private void getVideosFromDirectoryNonRecur(File _dir) {
       Drawable folderIcon = this.getResources().getDrawable(R.drawable.normalfolder);
       //add the
       if (!_dir.isDirectory()) {
           return;
       }
       File[] files = _dir.listFiles();
       if (files == null) {
           return;
       }
       Drawable videoIcon = null;
       int l_iconType = 0;
       for (File currentFile : files) {
           if (currentFile.isDirectory()) {
               //if it&#39;s a directory
               this.displayEntries.add(new IconifiedTextSelected(
                       currentFile.getPath(),
                       folderIcon, false, false, 0));
           } else {
               String l_filename = currentFile.getName();
               if (checkEndsWithInStringArray(l_filename,
                           getResources().getStringArray(R.array.fileEndingVideo))) {
                   if (number_of_icons &lt; 10) {
                       videoIcon = null;
                       ++number_of_icons;
                       l_iconType = 22;
                   } else {
                       videoIcon = null;
                       l_iconType = 2;
                   }
                   this.displayEntries.add(new IconifiedTextSelected(
                           currentFile.getPath(),
                           videoIcon, false, false, l_iconType));
               }
           }
       }
    }

    private void getVideosFromDirectoryRecur(File _dir) {
       Drawable videoIcon = null;
       File[] files = _dir.listFiles();
       int l_iconType = 2;
       if (files == null) {
           return;
       }
       for (File currentFile : files) {
           if (currentFile.isDirectory()) {
               getVideosFromDirectoryRecur(currentFile);
               continue;
           } else {
               String l_filename = currentFile.getName();
               //if it&#39;s an image file
               if (checkEndsWithInStringArray(l_filename,
                       getResources().getStringArray(R.array.fileEndingVideo))) {
                   if (number_of_icons &lt; 10) {
                       videoIcon = null;
                       ++number_of_icons;
                       l_iconType = 22;
                   } else {
                       videoIcon = null;
                       l_iconType = 2;
                   }
                   this.displayEntries.add(new IconifiedTextSelected(
                           currentFile.getPath(),
                           videoIcon, false, false, l_iconType));
               }
           }
       }
    }

    private void getVideosFromGallery() {
       Drawable videoIcon = null;
       Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
       String[] projection = {MediaStore.Video.Media.DATA};
       Cursor l_cursor = this.managedQuery(uri, projection, null, null, null);
       int videoNameColumnIndex;
       String videoFilename;
       File videoFile;
       int l_iconType = 2;
       if (l_cursor!=null) {
           if (l_cursor.moveToFirst()) {
               do {
                   videoNameColumnIndex = l_cursor.getColumnIndexOrThrow(
                           MediaStore.Images.Media.DATA);
                   videoFilename = l_cursor.getString(videoNameColumnIndex);
                   videoFile = new File(videoFilename);
                   if (!videoFile.exists()) {
                       continue;
                   }
                   if (number_of_icons &lt;= 10) {
                       videoIcon = null;
                       ++number_of_icons;
                       l_iconType = 22;
                   } else {
                       videoIcon = null;
                       l_iconType = 2;
                   }
                   this.displayEntries.add(new IconifiedTextSelected(
                           videoFile.getAbsolutePath(),
                           videoIcon, false, false, l_iconType));
               } while (l_cursor.moveToNext());
           }
       }
       if (l_cursor!=null) {
           l_cursor.close();
       }
    }

    private boolean checkEndsWithInStringArray(String checkItsEnd,
           String[] fileEndings){
       for(String aEnd : fileEndings){
           if(checkItsEnd.endsWith(aEnd))
               return true;
       }
       return false;
    }

    private class LoadVideoTask extends AsyncTask {
       @Override
       protected void onPreExecute() {
           System.gc();
           displayEntries.clear();
           showDialog(DIALOG_LOAD_MEDIA);
       }
       @Override
       protected Void doInBackground(String... params) {
           File l_root = new File(params[0]);
           if (l_root.isDirectory()) {
               number_of_icons = 0;
               currentDirectory = l_root;
               if (media_browser_load_option == 0) {
                   //list all videos in the root directory without going into sub folder
                   getVideosFromDirectoryNonRecurAddParent(l_root);
                   getVideosFromDirectoryNonRecur(l_root);
               } else if (media_browser_load_option == 1) {
                   //list all videos in the root folder recursively
                   getVideosFromDirectoryRecur(l_root);
               } else if (media_browser_load_option == 2) {
                   //list all videos in the gallery
                   getVideosFromGallery();
               }
           }
           return null;
       }
       @Override
       protected void onPostExecute(Void n) {
           refreshUI();
           dismissDialog(DIALOG_LOAD_MEDIA);
       }
    }

    /**
    * UI: this part of the source code deals with UI
    */
    //bottom menu
    private int currentFocusedBtn = 1;
    private Button btn_bottommenu1;
    private Button btn_bottommenu2;
    private Button btn_bottommenu3;
    //private Button btn_bottommenu4;
    //title bar
    private TextView text_titlebar_text;

    private void initUI() {
       this.requestWindowFeature(Window.FEATURE_NO_TITLE);
       this.setContentView(R.layout.video_browser);
       //title bar
       text_titlebar_text = (TextView) findViewById(R.id.titlebar_text);
       text_titlebar_text.setText("Click a video to display info");

       //bottom menu
       int l_btnWidth = this.getWindowManager().getDefaultDisplay().getWidth()/4;
       btn_bottommenu1 = (Button) findViewById(R.id.video_browser_btn1);
       //btn_bottommenu1 = (ActionMenuButton) findViewById(R.id.main_topsecretimport_btn1);
       btn_bottommenu1.setWidth(l_btnWidth);
       btn_bottommenu1.setOnClickListener(new View.OnClickListener() {
           public void onClick(View v) {
               btn_bottommenu1.setEnabled(false);
               btn_bottommenu2.setEnabled(true);
               btn_bottommenu3.setEnabled(true);
               currentFocusedBtn = 1;
               last_list_view_pos = 0;
               media_browser_load_option = 2;
               last_media_browser_load_option = media_browser_load_option;
               loadVideosFromDirectory("/sdcard/");
           }
       });
       btn_bottommenu2 = (Button) findViewById(R.id.video_browser_btn2);
       btn_bottommenu2.setWidth(l_btnWidth);
       btn_bottommenu2.setOnClickListener(new View.OnClickListener() {
           public void onClick(View v) {
               btn_bottommenu1.setEnabled(true);
               btn_bottommenu2.setEnabled(false);
               btn_bottommenu3.setEnabled(true);
               currentFocusedBtn = 2;
               last_list_view_pos = 0;
               media_browser_load_option = 0;
               last_media_browser_load_option = media_browser_load_option;
               loadVideosFromDirectory("/sdcard/");
           }
       });
       btn_bottommenu3 = (Button) findViewById(R.id.video_browser_btn3);
       btn_bottommenu3.setWidth(l_btnWidth);
       btn_bottommenu3.setOnClickListener(new View.OnClickListener() {
           public void onClick(View v) {
               btn_bottommenu1.setEnabled(true);
               btn_bottommenu2.setEnabled(true);
               btn_bottommenu3.setEnabled(false);
               currentFocusedBtn = 3;
               last_list_view_pos = 0;
               media_browser_load_option = 1;
               last_media_browser_load_option = media_browser_load_option;
               loadVideosFromDirectory("/sdcard/");
           }
       });
       media_browser_load_option = last_media_browser_load_option;
       if (media_browser_load_option==2) {
           btn_bottommenu1.setEnabled(false);
       } else if (media_browser_load_option==0) {
           btn_bottommenu2.setEnabled(false);
       } else if (media_browser_load_option==1){
           btn_bottommenu3.setEnabled(false);
       }
       loadVideosFromDirectory("/sdcard/");
    }
    //refresh the UI when the directoryEntries changes
    private static int last_list_view_pos = 0;
    public void refreshUI() {
       int l_btnWidth = this.getWindowManager().getDefaultDisplay().getWidth()/4;
       btn_bottommenu1.setWidth(l_btnWidth);
       btn_bottommenu2.setWidth(l_btnWidth);
       btn_bottommenu3.setWidth(l_btnWidth);
       //btn_bottommenu4.setWidth(l_btnWidth);

       SlowAdapter itla = new SlowAdapter(this);
       itla.setListItems(this.displayEntries);    
       this.setListAdapter(itla);
       getListView().setOnScrollListener(this);
       int l_size = this.displayEntries.size();
       if (l_size > 50) {
           getListView().setFastScrollEnabled(true);
       } else {
           getListView().setFastScrollEnabled(false);
       }
       if (l_size > 0) {
           if (last_list_view_pos &lt; l_size) {
               getListView().setSelection(last_list_view_pos);
           } else {
               getListView().setSelection(l_size-1);
           }
       }
       registerForContextMenu(getListView());
    }

    @Override
    public void onConfigurationChanged (Configuration newConfig) {
       super.onConfigurationChanged(newConfig);
       refreshUI();
    }

    static final int DIALOG_LOAD_MEDIA = 1;
    static final int DIALOG_HELP = 2;
    @Override
    protected Dialog onCreateDialog(int id) {
       switch(id) {
       case DIALOG_LOAD_MEDIA:
           ProgressDialog dialog = new ProgressDialog(this);
           dialog.setTitle("Load Files");
           dialog.setMessage("Please wait while loading...");
           dialog.setIndeterminate(true);
           dialog.setCancelable(true);
           return dialog;
       default:
           return null;
       }
    }
    /**
    * scroll events methods: this part of the source code contain the control source code
    * for handling scroll events
    */
    private boolean mBusy = false;
    private void disableButtons() {
       btn_bottommenu1.setEnabled(false);
       btn_bottommenu2.setEnabled(false);
       btn_bottommenu3.setEnabled(false);
    }

    private void enableButtons() {
       if (currentFocusedBtn!=1) {
           btn_bottommenu1.setEnabled(true);
       }
       if (currentFocusedBtn!=2) {
           btn_bottommenu2.setEnabled(true);
       }
       if (currentFocusedBtn!=3) {
           btn_bottommenu3.setEnabled(true);
       }
    }
    public void onScroll(AbsListView view, int firstVisibleItem,
           int visibleItemCount, int totalItemCount) {
       last_list_view_pos = view.getFirstVisiblePosition();
    }

    //private boolean mSaveMemory = false;
    public void onScrollStateChanged(AbsListView view, int scrollState) {      
       switch (scrollState) {
       case OnScrollListener.SCROLL_STATE_IDLE:
           enableButtons();
           mBusy = false;
           int first = view.getFirstVisiblePosition();
           int count = view.getChildCount();
           int l_releaseTarget;
           for (int i=0; i/if outofmemory, we try to clean up 10 view image resources,
                       //and try again
                       for (int j = 0; j &lt; 10; ++j) {
                           l_releaseTarget = first - count - j;
                           if (l_releaseTarget > 0) {
                               IconifiedTextSelected l_its = displayEntries.get(l_releaseTarget);
                               IconifiedTextSelectedView l_itsv = (IconifiedTextSelectedView)
                                   this.getListView().getChildAt(l_releaseTarget);
                               if (l_itsv!=null) {
                                   l_itsv.setIcon(null);
                               }
                               if (l_its != null) {
                                   Drawable l_dr = l_its.getIcon();
                                   l_its.setIcon(null);
                                   if (l_dr != null) {
                                       l_dr.setCallback(null);
                                       l_dr = null;
                                   }
                               }
                           }
                       }
                       System.gc();
                       //after clean up, we try again
                       if (l_type == 1) {
                           l_icon = null;
                       } else if (l_type == 2) {
                           l_icon = null;
                       }
                   }
                   this.displayEntries.get(first+i).setIcon(l_icon);
                   if (l_icon != null) {
                       t.setIcon(l_icon);
                       t.setTag(null);
                   }
               }
           }
           //System.gc();
           break;
       case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
           disableButtons();
           mBusy = true;
           break;
       case OnScrollListener.SCROLL_STATE_FLING:
           disableButtons();
           mBusy = true;
           break;
       }
    }

    /**
    * List item click action
    */
    private File currentFile;
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
       super.onListItemClick(l, v, position, id);
       last_list_view_pos = position;
       String selectedFileOrDirName = this.displayEntries.get((int)id).getText();
       if (selectedFileOrDirName.equals(upOneLevel)) {
           if (this.currentDirectory.getParent()!=null) {
               last_list_view_pos = 0;
               browseTo(this.currentDirectory.getParentFile());
           }
       } else {
           File l_clickedFile = new File(this.displayEntries.get((int)id).getText());
           if (l_clickedFile != null) {
               if (l_clickedFile.isDirectory()) {
                   last_list_view_pos = 0;
                   browseTo(l_clickedFile);
               } else {
                   showVideoInfo(l_clickedFile);
               }
           }
       }
    }

    private void browseTo(final File _dir) {
       if (_dir.isDirectory()) {
           this.currentDirectory = _dir;
           loadVideosFromDirectory(_dir.getAbsolutePath());
       }
    }

    /**
    * Slow adapter: this part of the code implements the list adapter
    * Will not bind views while the list is scrolling
    */
    private class SlowAdapter extends BaseAdapter {
       /** Remember our context so we can use it when constructing views. */
       private Context mContext;

       private List<iconifiedtextselected> mItems = new ArrayList<iconifiedtextselected>();

       public SlowAdapter(Context context) {
           mContext = context;
       }

       public void setListItems(List<iconifiedtextselected> lit)
       { mItems = lit; }

       /** @return The number of items in the */
       public int getCount() { return mItems.size(); }

       public Object getItem(int position)
       { return mItems.get(position); }

       /** Use the array index as a unique id. */
       public long getItemId(int position) {
           return position;
       }

       /** @param convertView The old view to overwrite, if one is passed
        * @returns a IconifiedTextSelectedView that holds wraps around an IconifiedText */
       public View getView(int position, View convertView, ViewGroup parent) {
           IconifiedTextSelectedView btv;
           if (convertView == null) {
               btv = new IconifiedTextSelectedView(mContext, mItems.get(position));
           } else { // Reuse/Overwrite the View passed
               // We are assuming(!) that it is castable!
               btv = (IconifiedTextSelectedView) convertView;
               btv.setText(mItems.get(position).getText());
           }
           if (position==0) {
               if (VideoBrowser.self.media_browser_load_option==0) {
                   btv.setIcon(R.drawable.folderback);
               } else if (mItems.get(0).getIcon()!=null) {
                   btv.setIcon(mItems.get(position).getIcon());
               } else {
                   btv.setIcon(R.drawable.video);
               }
           }
           //in busy mode
           else if (mBusy){
               //if icon is NULL: the icon is not loaded yet; load default icon
               if (mItems.get(position).getIcon()==null) {
                   btv.setIcon(R.drawable.video);
                   //mark this view, indicates the icon is not loaded
                   btv.setTag(this);
               } else {
                   //if icon is not null, just display the icon
                   btv.setIcon(mItems.get(position).getIcon());
                   //mark this view, indicates the icon is loaded
                   btv.setTag(null);
               }
           } else {
               //if not busy
               Drawable d = mItems.get(position).getIcon();
               if (d == null) {
                   //icon is not loaded, load now
                   btv.setIcon(R.drawable.video);
                   btv.setTag(this);
               } else {
                   btv.setIcon(mItems.get(position).getIcon());
                   btv.setTag(null);
               }
           }
           return btv;
       }
    }
    </iconifiedtextselected></iconifiedtextselected></iconifiedtextselected></iconifiedtextselected></iconifiedtextselected></iconifiedtextselected></iconifiedtextselected>

    }

  • Museum of Multimedia Software, Part 2

    16 août 2010, par Multimedia Mike — Software Museum

    This installment includes a bunch of old, discontinued Adobe software as well as some Flash-related mutlimedia software.

    Screen Time for Flash Screen Saver Factory
    "Create High Impact Screen Savers Using Macromedia Flash."



    Requirements include Windows 3.1, 95 or NT 3.5.1. A 486 computer is required to play the resulting screensavers which are Flash projectors using Macromedia Flash 3.0.

    Monster Interactive Instant GUI 2
    Create eye-popping GUIs more easily for use in Flash. Usability experts would argue that this is not a good thing.



    Adobe Dimensions 3.0
    "The Easy Yet Powerful 3D Rendering Tool." This software was end-of-life’d in late 2004-early 2005 (depending on region).



    Adobe ImageStyler
    "Instantly add style to your Web site." Wikipedia claims that this product was sold from 1998 to 2000 when it was superseded by Adobe LiveMotion (see below).



    Google is able to excavate a link to the Latin American site for Adobe ImageStyler, a page that doesn’t seem to be replicated in any other language.

    Adobe LiveMotion
    "Professional Web graphics and animation." This is version 1, where the last version was #2, released in 2002.



    Adobe Streamline 4.0
    "The most powerful way to convert images into line art." This was discontinued in mid-2005.



    Adobe SuperATM
    "The magic that maintains the look of your documents." This is the oldest item in my collection. A close examination of the back of the box reveals an old Adobe logo. The latest copyright date on the box is 1992.



  • Metal Gear Solid VP3 Easter Egg

    4 août 2011, par Multimedia Mike — Game Hacking

    Metal Gear Solid : The Twin Snakes for the Nintendo GameCube is very heavy on the cutscenes. Most of them are animated in real-time but there are a bunch of clips — normally of a more photo-realistic nature — that the developers needed to compress using a conventional video codec. What did they decide to use for this task ? On2 VP3 (forerunner of Theora) in a custom transport format. This is only the second game I have seen in the wild that uses pure On2 VP3 (first was a horse game). Reimar and I sorted out most of the details sometime ago. I sat down today and wrote a FFmpeg / Libav demuxer for the format, mostly to prove to myself that I still could.

    Things went pretty smoothly. We suspected that there was an integer field that indicated the frame rate, but 18 fps is a bit strange. I kept fixating on a header field that read 0x41F00000. Where have I seen that number before ? Oh, of course — it’s the number 30.0 expressed as an IEEE 32-bit float. The 4XM format pulled the same trick.

    Hexadecimal Easter Egg
    I know I finished the game years ago but I really can’t recall any of the clips present in the samples directory. The file mgs1-60.vp3 contains a computer screen granting the player access and illustrates this with a hexdump. It looks something like this :



    Funny, there are only 22 bytes on a line when there should be 32 according to the offsets. But, leave it to me to try to figure out what the file type is, regardless. I squinted and copied the first 22 bytes into a file :

     1F 8B 08 00   85 E2 17 38   00 03 EC 3A   0D 78 54 D5
     38 00 03 EC   3A 0D
    

    And the answer to the big question :

    $ file mgsfile
    mgsfile : gzip compressed data, from Unix, last modified : Wed Oct 27 22:43:33 1999
    

    A gzip’d file from 1999. I don’t know why I find this stuff so interesting, but I do. I guess it’s no more and less strange than writing playback systems like this.