3

I have created a console application that creates text anywhere in the console screen. I wanted to create a typewriter like effect, so i imported a keystroke sound from a typewriter and used it in my project. It was very hard to sync the sound to play exactly when the character is being typed in the screen, so i created a class called Sounds that creates a background thread for each of the sounds i want to run in the background.

Now that my characters are synced to the sound of the typewriter, i added a new sound file. This file should play whenever there is a new line. The problem i am facing now, is that the new typewriter carriage return sound is playing and suddenly stopping. To fix this, i added the PlaySync() command on the SoundPlayer instance. This allowed me to play the new file on the background but when the next message is executed, the carriage return sound is still playing while the characters are being typed to the console. After the carriage return is over, the keystroke sound resumes as normal.

I figured out why this is happening: PlaySync() will ensure that the sound is loaded and played and then resume with normal operations. If i use anything other than PlaySync the carriage return is to fast to even be heard. I have tried to add a delay but it is still not perfect. I want to be able to hear the sound of the keystroke being played as soon as the character is typed. When a new line is executed, i want to be able to hear the carriage return sound. All process must wait after this carriage return sound has completed its loop. What is the correct way of syncing these sounds? Is my logic flawed?

Screen.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace CanizHospital
{
    public class Screen
    {
        private Sounds sounds;
        private const int delay = 300;
        private static int _leftPos;
        private static int _topPos;

        public Screen(int leftPos, int topPos, int screenWidth, int screenHeight)
        {
            _leftPos = leftPos;
            _topPos = topPos;
            sounds = new Sounds();
            SetUpScreen(screenWidth, screenHeight);
        }

        private static void SetUpScreen(int width, int height)
        {
            IntPtr ptr = GetConsoleWindow();
            MoveWindow(ptr, 0, 0, 1000, 400, true);
            Console.SetWindowSize(width, height);
        }

        public void WriteAt(string message, int x, int y, bool typeWritter)
        {
            try
            {
                Console.SetCursorPosition(_leftPos + x, _topPos + y);
                if(typeWritter && message != null)
                {
                    TypeWritter(message, delay);
                }
            }
            catch(ArgumentOutOfRangeException e)
            {
                Console.Clear();
                Console.Beep(37, 500);
                Console.Write(e.Message);
            }
        }

        public void TypeWritter(string message, int delay, bool newLine = true)
        {

            foreach (char c in message)
            {

                Console.Write(c);
                sounds.LoadTypewriterSound();
                Thread.Sleep(delay);
            }


            if(newLine)
            {    
                Console.Write(Environment.NewLine);
                sounds.LoadCarriageReturn();
                Thread.Sleep(delay);
            }    
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr GetConsoleWindow();

        [DllImport("user32.dll", SetLastError = true)]
        internal static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
    }
}

Sounds.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Media;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace CanizHospital
{
    class Sounds
    {

        public void LoadTypewriterSound()
        {
            Thread backgroundSound = new Thread(new ThreadStart(PlayKey));
            backgroundSound.IsBackground = true;
            backgroundSound.Start();
        }

        public void LoadCarriageReturn()
        {
            Thread backgroundSound = new Thread(new ThreadStart(PlayCarriageReturn));
            backgroundSound.IsBackground = true;
            backgroundSound.Start();
        }

        private static void PlayKey()
        {
            SoundPlayer player = new SoundPlayer();
            player.SoundLocation = @"C:\Users\Erick\Desktop\C#\CanizHospital\CanizHospital\typewriter-key-1.wav";
            player.Play();
        }

        private static void PlayCarriageReturn()
        {
            SoundPlayer player = new SoundPlayer();
            player.SoundLocation = @"C:\Users\Erick\Desktop\C#\CanizHospital\CanizHospital\typewriter-return-1.wav";
            player.PlaySync();
        }
    }
}

Main

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Threading;
using Console = Colorful.Console;
using Colorful;

namespace CanizHospital
{
    class Program
    {

        static void Main(string[] args)
        {
            Screen screen = new Screen(Console.CursorLeft, Console.CursorTop, 
                Console.LargestWindowWidth, Console.LargestWindowHeight);


            screen.WriteAt("Hi whats up", 0, 0, true);
            //Thread.sleep(500);  //Delay here wont stop process
            screen.WriteAt("Hi whats up", 1, 1, true);
        }
    }
}
Erick Ramirez
  • 157
  • 1
  • 3
  • 9
  • 2
    `using` automatically calls `Dispose()` on the object, so I would guess that it is cutting the sound short when you use the async `Play()` because of that. – Herohtar Jan 17 '18 at 22:53
  • Hi, i used the using directive it seems in error. I had thought that SoundPlayer inherits IDisposable but that is wrong now that i look at the docs. I deleted both using directives and just implemented the instance of SoundPlayer. I still have the same issue. I believe the issue lies in between message executions. When the next statement is called, the carriage return still plays. Only after it is done will the keystroke sounds resume. I had originally thought that a simple delay between messages back on Main() would do it but im dead wrong. – Erick Ramirez Jan 17 '18 at 23:16
  • updated the code and added Main for clarification. – Erick Ramirez Jan 17 '18 at 23:21
  • 1
    Hm, in that case the only other thing I can see is that `SoundPlayer` uses `PlaySound()` from winmm.dll, and according to the API it will stop any currently playing sound in the same process. So if your program goes on to play another sound it will cut off anything else that is playing. Since you can't make `SoundPlayer` pass the flag that tells it to not stop currently playing sounds then you just have to wait until it's finished before continuing. – Herohtar Jan 17 '18 at 23:51
  • Exactly, so immediately after the carriage return sound is played, i need to halt the second message from executing to prevent the characters from typing and let the carraige return sound finish. Only then will the message execute. Seems simple enough right? Well if you take a look at Main() that delay should be enough to let the sound finish. It seems like the delay in Main() is ignored. I will try other solutions such as adding a flag. – Erick Ramirez Jan 17 '18 at 23:58

1 Answers1

2

First, you do not need to create a new thread to hold a SoundPlayer instance to call Play(). You could just call Play() before Console.Write and call Stop() after some delay (or you could not hear anything because it stops too fast). From MSDN, Play() method

Plays the .wav file using a new thread, and loads the .wav file first if it has not been loaded.

Second, PlaySync() blocks execution before it finishes, which meet your requirement exactly:

The PlaySync method uses the current thread to play a .wav file, preventing the thread from handling other messages until the load is complete.

Below is the code snippet that works the way you required:

public void TypeWritter(string message, int delay, bool newLine = true)
{
    var player = new SoundPlayer
    {
        SoundLocation = @"C:\Users\Erick\Desktop\C#\CanizHospital\CanizHospital\typewriter-key-1.wav"
    };
    foreach (char c in message)
    {
        player.Play();
        Console.Write(c);
        Thread.Sleep(delay);
        player.Stop();
    }
    if (newLine)
    {
        Console.Write(Environment.NewLine);
        player.SoundLocation = @"C:\Users\Erick\Desktop\C#\CanizHospital\CanizHospital\typewriter-return-1.wav";
        player.PlaySync();
        //Thread.Sleep(delay); // Might not be necessary
    }
}
Han Zhao
  • 1,932
  • 7
  • 10
  • Perfect! Thanks for the solution and your explanation i really appreciate it. I was going around in circles the whole time huh. – Erick Ramirez Jan 18 '18 at 00:18