This content originally appeared on DEV Community and was authored by HarmonyOS
Read the original article:Voice Notes App with ArkTS-1
Introduction
Hello everybody! Are you ready to build your very own voice notes application? If so, you are in the correct place. In this series, you will learn how to record and play sounds with ArkTS.
Recorder
We will start by implementing the recorder.
User Interface
- First, we need the UI. Create RecorderPage and place it under the components folder. (The components folder doesn’t exist, start with it.) This will give us a circular red button in the middle of the page that changes to a rectangle when a recording starts.
@Component
export default struct RecorderPage {
@State recording: boolean = false
build() {
Stack() {
Button() {
Stack() {
if (this.recording) {
Rect({ width: '35%', height: '35%' })
.fill(Color.Red)
} else {
Circle({ width: '90%', height: '90%' })
.fill(Color.Red)
}
}
.size({ width: '100%', height: '100%' })
.align(Alignment.Center)
}
.type(ButtonType.Circle)
.backgroundColor(Color.Transparent)
.size({ width: '50%', height: '50%' })
.border({ color: Color.White, width: 5 })
.onClick(async () => {
this.recording = !this.recording
})
}
}
}
- Modify the Index page to use the recorder page.
import RecorderPage from '../components/RecorderPage'
@Entry
@Component
struct Index {
build() {
Swiper() {
RecorderPage()
}
.height('100%')
.width('100%')
}
}
The result should look like this:
- Let’s smooth the transition. We can achieve a smooth transition using the scale transition effect.
Circle({ width: '90%', height: '90%' })
.fill(Color.Red)
.transition( // add this
TransitionEffect.asymmetric(
TransitionEffect.scale({ x: 0.5, y: 0.5 }).animation({ duration: 200 }),
TransitionEffect.scale({ x: 0.35, y: 0.35 }).animation({ duration: 200 }),
)
)
Now we have a smoother transition:
Permission
We need to get microphone permission to be able to use the device’s microphone.
- Define permission on the module.json5 file.
"requestPermissions": [
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:mic_reason",
"usedScene": {
"when": "inuse"
}
}
]
- Modify Index.ets to request permission from the user.
onDidBuild(): void {
this.requestMicPermission()
}
async requestMicPermission() {
const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
const context = this.getUIContext().getHostContext()
const permissions: Permissions[] = ['ohos.permission.MICROPHONE'];
try {
// request permisison from user
let granted =
(await (atManager.requestPermissionsFromUser(context,
permissions) as Promise<PermissionRequestResult>)).authResults[0] == 0
while (!granted) {
// request permission on settings
granted = (await atManager.requestPermissionOnSetting(context, permissions))[0] ==
abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
}
}
The app should ask for permission when it is first opened.
- We will use AVRecorder to record sounds. Create Recorder.ets and place it under the service directory.
import { media } from "@kit.MediaKit";
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from "@kit.BasicServicesKit";
@Observed
export default class Recorder {
private recorder: media.AVRecorder | undefined = undefined;
private fileName: string = ''
recorderState: media.AVRecorderState = 'idle'
get readyToRecord(): boolean {
return this.recorderState == 'idle' || this.recorderState == 'released'
}
get recording(): boolean {
return this.recorderState == 'started'
}
async start(context: Context) {
// initialize recorder
try {
this.recorder = await media.createAVRecorder()
// Callback function for state changes.
this.recorder.on('stateChange', (state: media.AVRecorderState, reason: media.StateChangeReason) => {
this.recorderState = state // track state changes
})
// Callback function for errors.
this.recorder.on('error', (err: BusinessError) => {
console.error(`avRecorder failed, code is ${err.code}, message is ${err.message}`);
})
let avProfile: media.AVRecorderProfile = {
audioBitrate: 100000, // Audio bit rate.
audioChannels: 2, // Number of audio channels.
audioCodec: media.CodecMimeType.AUDIO_AAC, // Audio encoding format.
audioSampleRate: 48000, // Audio sampling rate.
fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // Container format.
};
let filePath: string = context.filesDir + `/${Date.now()}.mp3`;
this.fileName = filePath.split('/').pop() as string
let audioFile: fs.File = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
let fileFd = audioFile.fd; // Obtain the file FD.
let avConfig: media.AVRecorderConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // Audio input source.
profile: avProfile,
url: 'fd://' + fileFd.toString(), // Obtain the file descriptor of the created audio file
};
await this.recorder.prepare(avConfig)
await this.recorder.start();
} catch (e) {
console.log('start err:', e.code, e.message)
}
}
async stopAndSave() {
try {
await this.recorder?.stop()
await this.recorder?.reset()
await this.recorder?.release();
this.recorder = undefined;
return this.fileName
} catch (e) {
console.log('stop err:', e.code, e.message)
throw new Error(e.message)
}
}
}
- Modify RecorderPage.ets to use the recorder.
@ObjectLink recorder: Recorder; // get recorder from parent, remove @State recording
// Buttton's onClick
.onClick(async () => {
if (this.recorder.readyToRecord) { // start the recording
await this.recorder.start(this.getUIContext().getHostContext()!)
} else if (this.recorder.recording) { // stop and save
await this.recorder.stopAndSave()
}
})
- Modify Index.ets, create the recorder object.
@State recorder: Recorder = new Recorder() // create the recorder object
RecorderPage({ recorder: this.recorder, records: this.records }) // pass down to the recorderPage
Now we can record voice, which will be stored in local app files.
Records
To see our records, we will create a basic UI that will list all voice notes. First, we will implement the Records class for data operations.
- Create Records.ets and place it under the service directory.
import { fileIo as fs } from '@kit.CoreFileKit';
@Observed
export default class Records {
fileNames: string[] = []
constructor(context: Context) {
this.fileNames = this._getAll(context)
}
private _getAll(context: Context): string[] {
try {
let filesDir = context.filesDir;
let files: string[] = fs.listFileSync(filesDir);
files.sort((a, b) => a.localeCompare(b))
return files
} catch (_) {
console.log(_)
return []
}
}
addNew(fileName: string) {
let copy = Array.from(this.fileNames)
copy.push(fileName)
this.fileNames = copy
}
}
- Now the UI. Create Notes.ets under the components directory. This will be a basic list to show our records.
import Records from '../service/Records';
import {
ArcList,
ArcListItem,
ArcListAttribute,
ArcListItemAttribute,
ComponentContent,
LengthMetrics
} from '@kit.ArkUI';
@Builder
function titleBuilder() {
Text('My Notes').fontSize(24)
}
@Component
export default struct Notes {
@ObjectLink records: Records
build() {
ArcList({
header: new ComponentContent(this.getUIContext(), wrapBuilder(titleBuilder))
}) {
ForEach(this.records.fileNames, (path: string, index: number) => {
ArcListItem() {
Button() {
Row() {
Text(new Date(Number.parseInt(path.split('.')[0])).toLocaleString())
Blank().layoutWeight(1)
SymbolGlyph($r('sys.symbol.arrow_right'))
}.padding({ left: 16, right: 16 })
}
.width('100%')
.height(40)
.backgroundColor(Color.White)
.foregroundColor(Color.Black)
.onClick(async () => {
// TODO: implement listen functionality
})
}.width('calc(100% - 32vp)')
}, (path: string) => path)
}.space(LengthMetrics.vp(8))
}
}
- Modify RecorderPage to add new notes to the Records.
@ObjectLink records: Records;
// Button's onClick function
const path = await this.recorder.stopAndSave()
this.records.addNew(path) // add this line
- Modify Index.ets, create the Records object.
@State records: Records = new Records(this.getUIContext().getHostContext()!)
// add Notes Page
Swiper() {
RecorderPage({ recorder: this.recorder, records: this.records })
Notes({ records: this.records }) // add Notes Page
}
Result
Conclusion
So far, we have implemented the recorder for our voice notes app. We will continue with the player in the second part. See you soon 🙂
Edit: Part 2 is here.
~ Fortuna Favet Fortibus
Written by Mehmet Karaaslan
This content originally appeared on DEV Community and was authored by HarmonyOS